From 826d5ce6264d19d9c2d6cf02cffb3fd13e925fbb Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Mon, 16 Dec 2024 12:59:33 +0100 Subject: [PATCH 1/3] model: add UTCDateTime --- invenio_banners/records/models.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/invenio_banners/records/models.py b/invenio_banners/records/models.py index c9238d1..8988edb 100644 --- a/invenio_banners/records/models.py +++ b/invenio_banners/records/models.py @@ -8,18 +8,37 @@ """Models.""" -from datetime import datetime +from datetime import datetime, timezone import sqlalchemy as sa from flask import current_app from invenio_db import db from sqlalchemy import or_ from sqlalchemy.sql import text +from sqlalchemy.types import DateTime, TypeDecorator from sqlalchemy_utils.models import Timestamp from ..services.errors import BannerNotExistsError +class UTCDateTime(TypeDecorator): + """Custom UTC datetime type.""" + + impl = DateTime + + def process_bind_param(self, value, dialect): + """Process value storing into database.""" + if isinstance(value, datetime): + return value.replace(tzinfo=None) + return value + + def process_result_value(self, value, dialect): + """Process value retrieving from database.""" + if isinstance(value, datetime): + return value.replace(tzinfo=None) + return value + + class BannerModel(db.Model, Timestamp): """Defines a message to show to users.""" @@ -36,10 +55,12 @@ class BannerModel(db.Model, Timestamp): category = db.Column(db.String(20), nullable=False) """Category of the message, for styling messages per category.""" - start_datetime = db.Column(db.DateTime, nullable=False, default=datetime.utcnow) + start_datetime = db.Column( + UTCDateTime, nullable=False, default=lambda: datetime.now(timezone.utc) + ) """Start date and time (UTC), can be immediate or delayed.""" - end_datetime = db.Column(db.DateTime, nullable=True) + end_datetime = db.Column(UTCDateTime, nullable=True) """End date and time (UTC), must be after `start` or forever if null.""" active = db.Column(db.Boolean(name="active"), nullable=False, default=True) From e078fcb5a17269c34b148569dfd69bc6877920c2 Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Mon, 16 Dec 2024 13:06:02 +0100 Subject: [PATCH 2/3] global: replace utcnow * this fixes DeprecationWarning for datetime.utcnow() --- invenio_banners/records/models.py | 4 ++-- invenio_banners/services/schemas.py | 3 ++- tests/records/test_disable.py | 11 ++++++----- tests/records/test_models.py | 17 +++++++++-------- tests/resources/test_resources.py | 14 +++++++------- tests/services/test_services.py | 16 ++++++++-------- tests/test_macro.py | 5 +++-- 7 files changed, 37 insertions(+), 33 deletions(-) diff --git a/invenio_banners/records/models.py b/invenio_banners/records/models.py index 8988edb..2045b14 100644 --- a/invenio_banners/records/models.py +++ b/invenio_banners/records/models.py @@ -111,7 +111,7 @@ def delete(cls, banner): @classmethod def get_active(cls, url_path): """Return active banners.""" - now = datetime.utcnow() + now = datetime.now(timezone.utc) query = ( db.session.query(cls) @@ -151,7 +151,7 @@ def search(cls, search_params, filters): @classmethod def disable_expired(cls): """Disable any old still active messages to keep everything clean.""" - now = datetime.utcnow() + now = datetime.now(timezone.utc) query = ( db.session.query(cls) diff --git a/invenio_banners/services/schemas.py b/invenio_banners/services/schemas.py index f5b868b..d1300c3 100644 --- a/invenio_banners/services/schemas.py +++ b/invenio_banners/services/schemas.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2022-2023 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Banners is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -22,7 +23,7 @@ class BannerSchema(BaseRecordSchema): category = fields.String(required=True, metadata={"default": "info"}) start_datetime = fields.DateTime( required=True, - metadata={"default": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")}, + metadata={"default": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")}, ) end_datetime = fields.DateTime(allow_none=True) active = fields.Boolean(required=True, metadata={"default": True}) diff --git a/tests/records/test_disable.py b/tests/records/test_disable.py index ec0fbd8..06550b2 100644 --- a/tests/records/test_disable.py +++ b/tests/records/test_disable.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Banners is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Test disable expired.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from invenio_banners.records.models import BannerModel @@ -16,28 +17,28 @@ "message": "valid", "url_path": "/valid", "category": "warning", - "end_datetime": datetime.utcnow() + timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) + timedelta(days=1), "active": True, }, "everywhere": { "message": "everywhere", "url_path": None, "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "sub_records_only": { "message": "sub_records_only", "url_path": "/resources/sub", "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "expired": { "message": "expired", "url_path": "/expired", "category": "info", - "end_datetime": datetime.utcnow() - timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, } diff --git a/tests/records/test_models.py b/tests/records/test_models.py index afd95be..df46984 100644 --- a/tests/records/test_models.py +++ b/tests/records/test_models.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2023 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Banners is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Test models.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest @@ -19,49 +20,49 @@ "message": "valid", "url_path": "/valid", "category": "info", - "end_datetime": datetime.utcnow() + timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) + timedelta(days=1), "active": True, }, "everywhere": { "message": "everywhere", "url_path": None, "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "with_end_datetime": { "message": "with_end_datetime", "url_path": "/with_end_datetime", "category": "info", - "end_datetime": datetime.utcnow() - timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "records_only": { "message": "records_only", "url_path": "/resources", "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "sub_records_only": { "message": "sub_records_only", "url_path": "/resources/sub", "category": "warning", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "disabled": { "message": "disabled", "url_path": "/disabled", "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": False, }, "expired": { "message": "expired", "url_path": "/expired", "category": "warning", - "end_datetime": datetime.utcnow() - timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, } diff --git a/tests/resources/test_resources.py b/tests/resources/test_resources.py index 57c643f..a9deb40 100644 --- a/tests/resources/test_resources.py +++ b/tests/resources/test_resources.py @@ -7,7 +7,7 @@ # under the terms of the MIT License; see LICENSE file for more details. """Banner resource tests.""" -from datetime import date, datetime, timedelta +from datetime import date, datetime, timedelta, timezone import pytest from invenio_db import db @@ -22,7 +22,7 @@ "category": "info", "active": True, "start_datetime": date(2022, 7, 20).strftime("%Y-%m-%d %H:%M:%S"), - "end_datetime": (datetime.utcnow() - timedelta(days=20)).strftime( + "end_datetime": (datetime.now(timezone.utc) - timedelta(days=20)).strftime( "%Y-%m-%d %H:%M:%S" ), }, @@ -32,7 +32,7 @@ "category": "info", "active": True, "start_datetime": date(2022, 7, 20).strftime("%Y-%m-%d %H:%M:%S"), - "end_datetime": (datetime.utcnow() + timedelta(days=20)).strftime( + "end_datetime": (datetime.now(timezone.utc) + timedelta(days=20)).strftime( "%Y-%m-%d %H:%M:%S" ), }, @@ -42,7 +42,7 @@ "category": "other", "active": False, "start_datetime": date(2022, 12, 15).strftime("%Y-%m-%d %H:%M:%S"), - "end_datetime": (datetime.utcnow() + timedelta(days=10)).strftime( + "end_datetime": (datetime.now(timezone.utc) + timedelta(days=10)).strftime( "%Y-%m-%d %H:%M:%S" ), }, @@ -52,7 +52,7 @@ "category": "warning", "active": True, "start_datetime": date(2023, 1, 20).strftime("%Y-%m-%d %H:%M:%S"), - "end_datetime": (datetime.utcnow() + timedelta(days=30)).strftime( + "end_datetime": (datetime.now(timezone.utc) + timedelta(days=30)).strftime( "%Y-%m-%d %H:%M:%S" ), }, @@ -146,7 +146,7 @@ def test_update_banner(client, admin, headers): admin.login(client) new_data = { - "start_datetime": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), + "start_datetime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"), "active": True, "message": "New banner message", "category": "info", @@ -170,7 +170,7 @@ def test_disable_expired_after_update_action(client, admin, headers): admin.login(client) new_data = { - "start_datetime": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), + "start_datetime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"), "active": True, "message": "New banner message", "category": "info", diff --git a/tests/services/test_services.py b/tests/services/test_services.py index 6cc9214..e54bf69 100644 --- a/tests/services/test_services.py +++ b/tests/services/test_services.py @@ -8,7 +8,7 @@ """Service level tests for Banners.""" -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import pytest from invenio_db import db @@ -23,8 +23,8 @@ "message": "active", "url_path": "/active", "category": "info", - "start_datetime": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), - "end_datetime": (datetime.utcnow() + timedelta(days=1)).strftime( + "start_datetime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"), + "end_datetime": (datetime.now(timezone.utc) + timedelta(days=1)).strftime( "%Y-%m-%d %H:%M:%S" ), "active": True, @@ -39,28 +39,28 @@ "message": "other", "url_path": "/other", "category": "warning", - "end_datetime": datetime.utcnow() + timedelta(days=5), + "end_datetime": datetime.now(timezone.utc) + timedelta(days=5), "active": True, }, "expired": { "message": "expired", "url_path": "/expired", "category": "info", - "end_datetime": datetime.utcnow() - timedelta(days=1), + "end_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "sub_records_only": { "message": "sub_records_only", "url_path": "/resources/sub", "category": "warning", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, "records_only": { "message": "records_only", "url_path": "/resources", "category": "info", - "start_datetime": datetime.utcnow() - timedelta(days=1), + "start_datetime": datetime.now(timezone.utc) - timedelta(days=1), "active": True, }, } @@ -90,7 +90,7 @@ def test_update_banner(app, superuser_identity): new_data = { "active": True, - "start_datetime": datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S"), + "start_datetime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"), "message": "New banner message", "category": "info", } diff --git a/tests/test_macro.py b/tests/test_macro.py index e661773..5709c57 100644 --- a/tests/test_macro.py +++ b/tests/test_macro.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2020-2023 CERN. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Banners is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Test views.""" -from datetime import datetime +from datetime import datetime, timezone import pytest from flask import url_for @@ -23,7 +24,7 @@ def _create_banner(message, category, url_path=None): "message": message, "category": category, "url_path": url_path, - "start_datetime": datetime.utcnow(), + "start_datetime": datetime.now(timezone.utc), "active": True, } ) From af5921fef76eed2eb626801d7a4151a0d4fb226c Mon Sep 17 00:00:00 2001 From: Christoph Ladurner Date: Mon, 16 Dec 2024 22:02:50 +0100 Subject: [PATCH 3/3] refactor: move UTCDateTime to invenio-db --- invenio_banners/records/models.py | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/invenio_banners/records/models.py b/invenio_banners/records/models.py index 2045b14..2b0c829 100644 --- a/invenio_banners/records/models.py +++ b/invenio_banners/records/models.py @@ -15,30 +15,11 @@ from invenio_db import db from sqlalchemy import or_ from sqlalchemy.sql import text -from sqlalchemy.types import DateTime, TypeDecorator from sqlalchemy_utils.models import Timestamp from ..services.errors import BannerNotExistsError -class UTCDateTime(TypeDecorator): - """Custom UTC datetime type.""" - - impl = DateTime - - def process_bind_param(self, value, dialect): - """Process value storing into database.""" - if isinstance(value, datetime): - return value.replace(tzinfo=None) - return value - - def process_result_value(self, value, dialect): - """Process value retrieving from database.""" - if isinstance(value, datetime): - return value.replace(tzinfo=None) - return value - - class BannerModel(db.Model, Timestamp): """Defines a message to show to users.""" @@ -56,11 +37,11 @@ class BannerModel(db.Model, Timestamp): """Category of the message, for styling messages per category.""" start_datetime = db.Column( - UTCDateTime, nullable=False, default=lambda: datetime.now(timezone.utc) + db.UTCDateTime, nullable=False, default=lambda: datetime.now(timezone.utc) ) """Start date and time (UTC), can be immediate or delayed.""" - end_datetime = db.Column(UTCDateTime, nullable=True) + end_datetime = db.Column(db.UTCDateTime, nullable=True) """End date and time (UTC), must be after `start` or forever if null.""" active = db.Column(db.Boolean(name="active"), nullable=False, default=True)