From d883b2a8f521df49cc213fedc30f5e60c0e5a530 Mon Sep 17 00:00:00 2001 From: Diederik van der Boor Date: Thu, 18 Jul 2024 13:38:59 +0200 Subject: [PATCH] Fix timezone passing to datetime, since Django 4.2 uses zoneinfo This ruff fix previously didn't work on Django 3.2 as that uses pytz. Passing such timezone to datetime causes +00:18 min shifts instead of the expected +01:00 change. This is caused by the way pytz handles the zoneinfo from history and needs pytz.timezone.localize() to create the proper timezone information. See: https://groups.google.com/g/django-users/c/rXalwEztfr0/m/QAd5bIJubwAJ --- pyproject.toml | 5 ++++- src/dso_api/dynamic_api/temporal.py | 6 ++++-- src/rest_framework_dso/renderers.py | 3 ++- src/tests/conftest.py | 18 ++++++++++++------ .../test_dynamic_api/test_temporal_actions.py | 8 ++++---- src/tests/test_dynamic_api/test_views_mvt.py | 4 ++-- 6 files changed, 28 insertions(+), 16 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0b7f5f88f..bafea9fbd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ select = [ "C90", # mccabe "BLE", # flake8-blind-except "C4", # flake8-comprehensions - # "DTZ", # flake8-datetimez + "DTZ", # flake8-datetimez "T10", # flake8-debugger "DJ", # flake8-django "ISC", # flake8-implicit-str-concat @@ -85,6 +85,9 @@ allow-dict-calls-with-keyword-arguments = true [tool.ruff.lint.flake8-gettext] extend-function-names = ["gettext_lazy", "ngettext_lazy", "pgettext", "pgettext_lazy", "npgettext", "npgettext_lazy"] +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"django.utils.timezone.make_aware".msg = "There is no need for make_aware(), pass tzinfo directly." + [tool.ruff.lint.isort] known-first-party = ["dso_api", "rest_framework_dso"] known-third-party = ["gisserver", "schematools"] diff --git a/src/dso_api/dynamic_api/temporal.py b/src/dso_api/dynamic_api/temporal.py index d06e0cdb3..4d57e273d 100644 --- a/src/dso_api/dynamic_api/temporal.py +++ b/src/dso_api/dynamic_api/temporal.py @@ -12,7 +12,7 @@ from django.db import models from django.db.models import Q -from django.utils.timezone import make_aware, now +from django.utils.timezone import get_current_timezone, now from more_itertools import first from rest_framework.exceptions import ValidationError from rest_framework.request import Request @@ -119,7 +119,9 @@ def _parse_date(dimension: str, value: str) -> Literal["*"] | date | datetime: try: if "T" in value or " " in value: - return make_aware(datetime.fromisoformat(value)) + # Add timezone if needed. + val = datetime.fromisoformat(value) + return val.replace(tzinfo=get_current_timezone()) if val.tzinfo is None else val else: return date.fromisoformat(value) except ValueError: diff --git a/src/rest_framework_dso/renderers.py b/src/rest_framework_dso/renderers.py index bce8f296b..a4ed24060 100644 --- a/src/rest_framework_dso/renderers.py +++ b/src/rest_framework_dso/renderers.py @@ -10,6 +10,7 @@ import orjson from django.conf import settings from django.urls import reverse +from django.utils.timezone import get_current_timezone from rest_framework import renderers from rest_framework.relations import HyperlinkedRelatedField from rest_framework.serializers import ListSerializer, Serializer, SerializerMethodField @@ -91,7 +92,7 @@ def finalize_response(self, response, renderer_context: dict): def get_http_headers(self, renderer_context: dict): """Return the http headers for the response.""" if self.content_disposition: - now = datetime.now().isoformat() + now = datetime.now(tz=get_current_timezone()).isoformat() dataset_id = renderer_context.get("dataset_id", "dataset") table_id = renderer_context.get("table_id", "table") return { diff --git a/src/tests/conftest.py b/src/tests/conftest.py index f1ec6cfc5..90b71350f 100644 --- a/src/tests/conftest.py +++ b/src/tests/conftest.py @@ -12,7 +12,7 @@ from django.core.handlers.wsgi import WSGIRequest from django.db import connection from django.utils.functional import SimpleLazyObject -from django.utils.timezone import make_aware +from django.utils.timezone import get_current_timezone from jwcrypto.jwt import JWT from psycopg2.sql import SQL, Identifier from rest_framework.request import Request @@ -26,8 +26,8 @@ from tests.utils import api_request_with_scopes, to_drf_request HERE = Path(__file__).parent -DATE_2021_FEB = make_aware(datetime(2021, 2, 28, 10, 0)) -DATE_2021_JUNE = make_aware(datetime(2021, 6, 11, 10, 0)) +DATE_2021_FEB = datetime(2021, 2, 28, 10, 0, tzinfo=get_current_timezone()) +DATE_2021_JUNE = datetime(2021, 6, 11, 10, 0, tzinfo=get_current_timezone()) @pytest.fixture() @@ -367,7 +367,7 @@ def afval_container(afval_container_model, afval_cluster): eigenaar_naam="Dataservices", # set to fixed dates to the CSV export can also check for desired formatting datum_creatie=date(2021, 1, 3), - datum_leegmaken=make_aware(datetime(2021, 1, 3, 12, 13, 14)), + datum_leegmaken=datetime(2021, 1, 3, 12, 13, 14, tzinfo=get_current_timezone()), cluster=afval_cluster, geometry=Point(10, 10), # no SRID on purpose, should use django model field. ) @@ -571,10 +571,16 @@ def movies_model(movies_dataset, dynamic_models): def movies_data(movies_model, movies_category): return [ movies_model.objects.create( - id=3, name="foo123", category=movies_category, date_added=datetime(2020, 1, 1, 0, 45) + id=3, + name="foo123", + category=movies_category, + date_added=datetime(2020, 1, 1, 0, 45, tzinfo=get_current_timezone()), ), movies_model.objects.create( - id=4, name="test", category=movies_category, date_added=datetime(2020, 2, 2, 13, 15) + id=4, + name="test", + category=movies_category, + date_added=datetime(2020, 2, 2, 13, 15, tzinfo=get_current_timezone()), ), ] diff --git a/src/tests/test_dynamic_api/test_temporal_actions.py b/src/tests/test_dynamic_api/test_temporal_actions.py index 5542a566d..bc7264448 100644 --- a/src/tests/test_dynamic_api/test_temporal_actions.py +++ b/src/tests/test_dynamic_api/test_temporal_actions.py @@ -1,4 +1,4 @@ -from datetime import date, datetime +from datetime import date, datetime, timezone from urllib.parse import parse_qs, urlparse import pytest @@ -18,7 +18,7 @@ def stadsdelen(gebieden_models): id="03630000000016.1", identificatie="03630000000016", volgnummer=1, - registratiedatum=datetime(2006, 6, 12, 5, 40, 12), + registratiedatum=datetime(2006, 6, 12, 5, 40, 12, tzinfo=timezone.utc), begin_geldigheid=date(2006, 6, 1), eind_geldigheid=date(2015, 1, 1), naam="Zuidoost", @@ -28,7 +28,7 @@ def stadsdelen(gebieden_models): id="03630000000016.2", identificatie="03630000000016", volgnummer=2, - registratiedatum=datetime(2015, 1, 1, 5, 40, 12), + registratiedatum=datetime(2015, 1, 1, 5, 40, 12, tzinfo=timezone.utc), begin_geldigheid=date(2015, 1, 1), eind_geldigheid=None, naam="Zuidoost", @@ -46,7 +46,7 @@ def gebied(gebieden_models, stadsdelen, buurt): id="03630950000019.1", identificatie="03630950000019", volgnummer=1, - registratiedatum=datetime(2015, 1, 1, 5, 40, 12), + registratiedatum=datetime(2015, 1, 1, 5, 40, 12, tzinfo=timezone.utc), begin_geldigheid=date(2014, 2, 20), naam="Bijlmer-Centrum", ) diff --git a/src/tests/test_dynamic_api/test_views_mvt.py b/src/tests/test_dynamic_api/test_views_mvt.py index 264248ec6..bb3a93456 100644 --- a/src/tests/test_dynamic_api/test_views_mvt.py +++ b/src/tests/test_dynamic_api/test_views_mvt.py @@ -3,7 +3,7 @@ import mapbox_vector_tile import pytest from django.contrib.gis.geos import Point -from django.utils.timezone import make_aware +from django.utils.timezone import get_current_timezone CONTENT_TYPE = "application/vnd.mapbox-vector-tile" @@ -119,7 +119,7 @@ def test_mvt_content(api_client, afval_container_model, afval_cluster, filled_ro eigenaar_naam="Dataservices", # set to fixed dates to the CSV export can also check for desired formatting datum_creatie=date(2021, 1, 3), - datum_leegmaken=make_aware(datetime(2021, 1, 3, 12, 13, 14)), + datum_leegmaken=datetime(2021, 1, 3, 12, 13, 14, tzinfo=get_current_timezone()), cluster=afval_cluster, geometry=Point(123207.6558130105, 486624.6399002579), )