Received: from mail.netlandish.com (mail.netlandish.com [174.136.98.166]) by code.netlandish.com (Postfix) with ESMTP id E6A2180B12 for <~petersanchez/public-inbox@lists.code.netlandish.com>; Wed, 8 Mar 2023 10:58:35 +0000 (UTC) Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=209.85.208.50; helo=mail-ed1-f50.google.com; envelope-from=hugo@yunojuno.com; receiver= Authentication-Results: mail.netlandish.com; dkim=pass (2048-bit key; unprotected) header.d=yunojuno-com.20210112.gappssmtp.com header.i=@yunojuno-com.20210112.gappssmtp.com header.b=2WyNgbjn Received: from mail-ed1-f50.google.com (mail-ed1-f50.google.com [209.85.208.50]) by mail.netlandish.com (Postfix) with ESMTP id 6FA31152E8A for <~petersanchez/public-inbox@lists.code.netlandish.com>; Wed, 8 Mar 2023 10:58:32 +0000 (UTC) Received: by mail-ed1-f50.google.com with SMTP id ay14so60324299edb.11 for <~petersanchez/public-inbox@lists.code.netlandish.com>; Wed, 08 Mar 2023 02:58:32 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=yunojuno-com.20210112.gappssmtp.com; s=20210112; t=1678273111; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject :date:message-id:reply-to; bh=Gmn3R0W6VqNO901TWrGIeR8QcJ3SCp0MwQLZRnqD6fM=; b=2WyNgbjn17YkdMVb6s2Qvud/IZYpzgIFoeaErIVdoISiTrurOmYxPcqNHD4iNIqw6F lOY34LBIdKfJyaVsS6r2TIUprT8GQWKTqWbqD0ZFWd8bqUtPxbS+d9N3Gb6R3fwQslWW uXCTZoQ5pta+ON7il2gpK+a1PeqJjMRXXpzRAntRpQLvXR5f1kXm73oDb8s6NE80Y/bP r1hFBGaqCX93eiE3+G3WVW9RuOKK+X3tTjYC4xOME4nRydWpW0a4sWKGZ6io8q+7Ffrg TnFTY+GCUkEW4VyRvfTg97PiMrTcK3/0oDPYkdKg8dNfHwC8klYAY8svvD4yo5GHSznK qMAg== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20210112; t=1678273111; h=to:subject:message-id:date:from:mime-version:x-gm-message-state :from:to:cc:subject:date:message-id:reply-to; bh=Gmn3R0W6VqNO901TWrGIeR8QcJ3SCp0MwQLZRnqD6fM=; b=1R5xHS2dzt3nVBq69m2wxkFhQKxV4o2+mwDlmBQFUZCUoJdu1s2Ktu3xyKu9GOmcmm JYZEgMD95XFPeCjEABI8QiLuFC9Jk4fP1j8J4dtWdWSSzYl6u7zJ4H0aPWnkeeV49GHJ Qr+Owa7g/po7icfEBxHHF+NwKkzOwps5k3t/aiWMG7EjUQPm4J3VZ0KXXEI+Cz0KG9sy uiHnuk6k8MaT7ELlkPMSMlQ1McmsIKrYbQCI6CglReWbqzTw+Aem23KPUcCjHTfurz36 wx3SQ55Fv40ZAjD6QHgP5uoc3USYz4tPM12uoO/NFA7tfrIb6qf7OVVTQoGcMgT2SAaR QsmA== X-Gm-Message-State: AO0yUKWYUxAAWdJ1reoNAHAeuLqsicOdMJUl2l0w7Q7NQHcRbXWuj9x1 AjYE046f/TvmfU9yBTPC+mmjvuwrZ4n9GOLsUhT+l1EBrEYd2U0RCbA= X-Google-Smtp-Source: AK7set92OmSJKD8oO3Bx5BT9ZlNmynZAwgwejBzPpfBThRdFE4Zoa3bxQStKJm4P8OSTzV9IH1FE29YILFZdSCcVTYs= X-Received: by 2002:a17:906:a05a:b0:8b2:23fb:dfd1 with SMTP id bg26-20020a170906a05a00b008b223fbdfd1mr9093140ejb.2.1678273111364; Wed, 08 Mar 2023 02:58:31 -0800 (PST) MIME-Version: 1.0 From: Hugo Rodger-Brown Date: Wed, 8 Mar 2023 10:58:20 +0000 Message-ID: Subject: [PATCH django-impersonate] Handle upcoming removal of timezone.utc (Django 5) To: ~petersanchez/public-inbox@lists.code.netlandish.com Content-Type: text/plain; charset="UTF-8" # HG changeset patch # User Hugo Rodger-Brown # Date 1678268809 0 # Wed Mar 08 09:46:49 2023 +0000 # Node ID c0c223b5e10524c9cad20de0b11d901277f5b89d # Parent 76e93d43501e5304d6b27e7f7072e4d8aab15104 Add compat module to handle removal of timzone.utc In Django 5 the way in which timezones are handled is updated, and a side-effect of this is that the `utc` value is removed from `django.utils.timezone`. I have created a new `compat.py` module to handle both scenarios and expose a value called `UTC` that can be used wherever a tzinfo is required. At the same time I have moved the `reverse` import code into the same module so it it centralised. I have also updated the build matrix to include Python 3.10, 3.11, and Django 4.1, "main". diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -6,10 +6,12 @@ **.pyc MANIFEST .idea +.venv +*.egg-info syntax:regexp ^htmlcov$ ^env$ syntax: glob *.komodoproject -.DS_Store \ No newline at end of file +.DS_Store diff --git a/MANIFEST.in b/MANIFEST.in --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include BSD-LICENSE include CHANGELOG +include LICENSE include README.rst recursive-include impersonate/templates * diff --git a/impersonate/admin.py b/impersonate/admin.py --- a/impersonate/admin.py +++ b/impersonate/admin.py @@ -5,15 +5,11 @@ from django.db.utils import NotSupportedError from django.utils.html import format_html +from .compat import reverse from .helpers import User, check_allow_impersonate from .models import ImpersonationLog from .settings import settings -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse - logger = logging.getLogger(__name__) diff --git a/impersonate/compat.py b/impersonate/compat.py new file mode 100644 --- /dev/null +++ b/impersonate/compat.py @@ -0,0 +1,15 @@ +try: + from django.urls import reverse +except ImportError: + from django.core.urlresolvers import reverse + +# timezone.utc removed in Django 5 +try: + from django.utils.timezone import utc + UTC = utc +except ImportError: + from zoneinfo import ZoneInfo + UTC = ZoneInfo("UTC") + + +__all__ = ["reverse", "UTC"] diff --git a/impersonate/middleware.py b/impersonate/middleware.py --- a/impersonate/middleware.py +++ b/impersonate/middleware.py @@ -3,10 +3,10 @@ from django.http import HttpResponseNotAllowed from django.shortcuts import redirect, reverse -from django.utils import timezone from django.utils.deprecation import MiddlewareMixin from django.utils.functional import SimpleLazyObject +from .compat import UTC from .helpers import User, check_allow_for_uri, check_allow_for_user, check_read_only from .settings import settings @@ -33,11 +33,11 @@ return start_time = datetime.fromtimestamp( - request.session['_impersonate_start'], timezone.utc + request.session['_impersonate_start'], UTC ) delta = timedelta(seconds=settings.MAX_DURATION) - if datetime.now(timezone.utc) - start_time > delta: + if datetime.now(UTC) - start_time > delta: return redirect('impersonate-stop') new_user_id = request.session['_impersonate'] diff --git a/impersonate/tests.py b/impersonate/tests.py --- a/impersonate/tests.py +++ b/impersonate/tests.py @@ -40,11 +40,7 @@ from .helpers import users_impersonable from .models import ImpersonationLog from .signals import session_begin, session_end - -try: - from django.urls import reverse -except ImportError: - from django.core.urlresolvers import reverse +from .compat import reverse, UTC User = get_user_model() django_version_loose = LooseVersion(django.get_version()) @@ -154,7 +150,7 @@ @override_settings(IMPERSONATE={'MAX_DURATION': 3600}) def test_impersonated_request_with_max_duration(self): self._impersonated_request( - _impersonate_start=datetime.now(timezone.utc).timestamp() + _impersonate_start=datetime.now(UTC).timestamp() ) @override_settings(IMPERSONATE={'MAX_DURATION': 5, 'REDIRECT_URL': '/foo/'}) @@ -163,12 +159,12 @@ See Issue #67 ''' self._impersonated_request( - _impersonate_start=datetime.now(timezone.utc).timestamp() + _impersonate_start=datetime.now(UTC).timestamp() ) # new request to see if the redirect to stop request = self.factory.get('/') request.user = self.superuser - past_time = datetime.now(timezone.utc) - timedelta(hours=1) + past_time = datetime.now(UTC) - timedelta(hours=1) request.session = { '_impersonate': self.user, '_impersonate_start': past_time.timestamp(), @@ -207,7 +203,7 @@ request.session = { '_impersonate': self.user.pk, '_impersonate_start': ( - datetime.now(timezone.utc) - timedelta(seconds=3601) + datetime.now(UTC) - timedelta(seconds=3601) ).timestamp(), } response = self.middleware.process_request(request) diff --git a/impersonate/views.py b/impersonate/views.py --- a/impersonate/views.py +++ b/impersonate/views.py @@ -5,7 +5,6 @@ from django.db.models import Q from django.http import Http404 from django.shortcuts import get_object_or_404, redirect, render -from django.utils import timezone from .decorators import allowed_user_required from .helpers import ( @@ -14,6 +13,7 @@ ) from .settings import User, settings from .signals import session_begin, session_end +from .compat import UTC logger = logging.getLogger(__name__) @@ -39,7 +39,7 @@ if check_allow_for_user(request, new_user): request.session['_impersonate'] = new_user.pk request.session['_impersonate_start'] = datetime.now( - tz=timezone.utc + tz=UTC ).timestamp() prev_path = request.META.get('HTTP_REFERER') if prev_path: diff --git a/pyproject.toml b/pyproject.toml --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.black] line-length = 79 skip-string-normalization = true -target-version = ['py37', 'py38'] +target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] include = '\.pyi?$' exclude = ''' diff --git a/setup.py b/setup.py --- a/setup.py +++ b/setup.py @@ -48,11 +48,13 @@ 'Framework :: Django :: 2.2', 'Framework :: Django :: 3.2', 'Framework :: Django :: 4.0', + 'Framework :: Django :: 4.1', 'Programming Language :: Python', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', 'Environment :: Web Environment', ], ) diff --git a/tox.ini b/tox.ini --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] downloadcache = {toxworkdir}/cache/ -envlist = py{37,38,39}-django{2.2,3.2},py{38,39}-django{4.0},py310-django{3.2,4.0} +envlist = py{37,38,39}-django{2.2,3.2},py{38,39}-django{4.0,4.1},py310-django{3.2,4.0,4.1},py311-django{3.2,4.0,4.1,main} [testenv] commands = {envpython} runtests.py @@ -8,3 +8,5 @@ django2.2: django>=2.2,<3.0 django3.2: django>=3.2,<4.0 django4.0: django>=4.0,<4.1 + django4.1: django>=4.1,<4.2 + djangomain: https://github.com/django/django/archive/main.tar.gz