diff --git a/invenio_communities/communities/dumpers/featured.py b/invenio_communities/communities/dumpers/featured.py index 19f6cf3c8..29d407cd8 100644 --- a/invenio_communities/communities/dumpers/featured.py +++ b/invenio_communities/communities/dumpers/featured.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # # This file is part of Invenio. -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -12,7 +12,7 @@ search body. """ -from datetime import datetime +from datetime import datetime, timezone from invenio_records.dumpers import SearchDumperExt @@ -28,7 +28,7 @@ def __init__(self, key="featured"): def dump(self, record, data): """Dump featured entries.""" - now_ = datetime.utcnow() + now_ = datetime.now(timezone.utc) future_entries = ( CommunityFeatured.query.filter( CommunityFeatured.community_id == record.id, diff --git a/invenio_communities/communities/records/models.py b/invenio_communities/communities/records/models.py index f4ad14d99..a17d53659 100644 --- a/invenio_communities/communities/records/models.py +++ b/invenio_communities/communities/records/models.py @@ -2,20 +2,19 @@ # # This file is part of Invenio. # Copyright (C) 2016-2021 CERN. -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Community database models.""" -from datetime import datetime +from datetime import datetime, timezone from invenio_db import db from invenio_files_rest.models import Bucket -from invenio_records.models import RecordMetadataBase, Timestamp +from invenio_records.models import RecordMetadataBase from invenio_records_resources.records import FileRecordModelMixin -from sqlalchemy.dialects import mysql from sqlalchemy.types import String from sqlalchemy_utils.types import ChoiceType, UUIDType @@ -48,7 +47,7 @@ class CommunityFileMetadata(db.Model, RecordMetadataBase, FileRecordModelMixin): __tablename__ = "communities_files" -class CommunityFeatured(db.Model, Timestamp): +class CommunityFeatured(db.Model, db.Timestamp): """Featured community entry.""" __tablename__ = "communities_featured" @@ -58,7 +57,7 @@ class CommunityFeatured(db.Model, Timestamp): UUIDType, db.ForeignKey(CommunityMetadata.id), nullable=False ) start_date = db.Column( - db.DateTime().with_variant(mysql.DATETIME(fsp=6), "mysql"), + db.UTCDateTime(), nullable=False, - default=datetime.utcnow, + default=lambda: datetime.now(timezone.utc), ) diff --git a/invenio_communities/communities/records/systemfields/tombstone.py b/invenio_communities/communities/records/systemfields/tombstone.py index 0e969ac81..d0d6842d0 100644 --- a/invenio_communities/communities/records/systemfields/tombstone.py +++ b/invenio_communities/communities/records/systemfields/tombstone.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Communities is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. """Systemfield for managing tombstone information of a community.""" -from datetime import datetime +from datetime import datetime, timezone from invenio_records.systemfields import SystemField from invenio_requests.resolvers.registry import ResolverRegistry @@ -90,7 +91,7 @@ def removal_date(self): def removal_date(self, value): """Set the removal date.""" if value is None: - value = datetime.utcnow() + value = datetime.now(timezone.utc) if isinstance(value, datetime): value = value.isoformat() diff --git a/invenio_communities/communities/resources/serializer.py b/invenio_communities/communities/resources/serializer.py index d254fd05f..faaf6d8bd 100644 --- a/invenio_communities/communities/resources/serializer.py +++ b/invenio_communities/communities/resources/serializer.py @@ -2,6 +2,7 @@ # # This file is part of Invenio. # Copyright (C) 2022 CERN. +# Copyright (C) 2025 Graz University of Technology. # # Invenio 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,5 +23,4 @@ def __init__(self): format_serializer_cls=JSONSerializer, object_schema_cls=UICommunitySchema, list_schema_cls=BaseListSchema, - schema_context={"object_key": "ui"}, ) diff --git a/invenio_communities/communities/resources/ui_schema.py b/invenio_communities/communities/resources/ui_schema.py index 3524adcbd..c9fd0e0bb 100644 --- a/invenio_communities/communities/resources/ui_schema.py +++ b/invenio_communities/communities/resources/ui_schema.py @@ -3,6 +3,7 @@ # This file is part of Invenio. # Copyright (C) 2022-2024 CERN. # Copyright (C) 2023 TU Wien. +# Copyright (C) 2025 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. @@ -93,6 +94,8 @@ class FundingSchema(Schema): class UICommunitySchema(BaseObjectSchema): """Schema for dumping extra information of the community for the UI.""" + object_key = "ui" + type = fields.Nested(VocabularyL10Schema, attribute="metadata.type") funding = fields.List( diff --git a/invenio_communities/fixtures/tasks.py b/invenio_communities/fixtures/tasks.py index c08e40445..e33d82dad 100644 --- a/invenio_communities/fixtures/tasks.py +++ b/invenio_communities/fixtures/tasks.py @@ -2,14 +2,15 @@ # # This file is part of Invenio. # Copyright (C) 2016-2021 CERN. -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2022-2025 Graz University of Technology. # # Invenio is free software; you can redistribute it and/or modify it # under the terms of the MIT License; see LICENSE file for more details. """Celery tasks for fixtures.""" -from datetime import datetime + +from datetime import datetime, timezone from uuid import UUID from celery import shared_task @@ -39,7 +40,7 @@ def create_demo_community(data, logo_path=None, feature=False): service.update_logo(system_identity, community.id, filestream) if feature: - featured_data = {"start_date": datetime.utcnow().isoformat()} + featured_data = {"start_date": datetime.now(timezone.utc).isoformat()} service.featured_create(system_identity, community.id, featured_data) if "id" in data: uuid = data["id"] @@ -54,7 +55,7 @@ def create_demo_community(data, logo_path=None, feature=False): def reindex_featured_entries(): """Reindexes records having at least one future entry which is now in the past.""" service = current_communities.service - now = datetime.utcnow().isoformat() + now = datetime.now(timezone.utc).isoformat() @unit_of_work() def reindex_community(hit, uow=None): diff --git a/invenio_communities/members/services/schemas.py b/invenio_communities/members/services/schemas.py index ad9f9f71f..32941cf07 100644 --- a/invenio_communities/members/services/schemas.py +++ b/invenio_communities/members/services/schemas.py @@ -2,7 +2,7 @@ # # Copyright (C) 2022 Northwestern University. # Copyright (C) 2022 CERN. -# Copyright (C) 2023 Graz University of Technology. +# Copyright (C) 2023-2025 Graz University of Technology. # # Invenio-Communities is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. @@ -18,6 +18,7 @@ current_users_service, ) from marshmallow import Schema, ValidationError, fields, validate, validates_schema +from marshmallow_utils.context import context_schema from marshmallow_utils.fields import SanitizedUnicode, TZDateTime from .fields import RoleField @@ -123,7 +124,7 @@ def get_user_member(self, user): name = profile.get("full_name") or user.get("username") or _("Untitled") description = profile.get("affiliations") or "" fake_user_obj = SimpleNamespace(id=user["id"]) - current_identity = self.context["identity"] + current_identity = context_schema.get()["identity"] avatar = current_users_service.links_item_tpl.expand( current_identity, fake_user_obj )["avatar"] @@ -139,7 +140,7 @@ def get_user_member(self, user): def get_group_member(self, group): """Get a group member.""" fake_group_obj = SimpleNamespace(id=group["id"]) - current_identity = self.context["identity"] + current_identity = context_schema.get()["identity"] avatar = current_groups_service.links_item_tpl.expand( current_identity, fake_group_obj )["avatar"] @@ -168,7 +169,7 @@ class MemberDumpSchema(PublicDumpSchema): def is_self(self, obj): """Get permission.""" if "is_self" not in self.context: - current_identity = self.context["identity"] + current_identity = context_schema.get()["identity"] self.context["is_self"] = ( obj.user_id is not None and current_identity.id is not None @@ -182,7 +183,7 @@ def get_current_user(self, obj): def get_permissions(self, obj): """Get permission.""" - permission_check = self.context["field_permission_check"] + permission_check = context_schema.get()["field_permission_check"] # Does not take CommunitySelfMember into account because no "member" is # passed to the permission check. @@ -214,7 +215,7 @@ def get_permissions(self, obj): # Only owners and managers can list invitations, and thus only the # request status is necessary to determine if the identity can cancel. is_open = obj["request"]["status"] == "submitted" - permission_check = self.context["field_permission_check"] + permission_check = context_schema.get()["field_permission_check"] return { "can_cancel": is_open, diff --git a/invenio_communities/members/services/service.py b/invenio_communities/members/services/service.py index 85bdba811..a2a664615 100644 --- a/invenio_communities/members/services/service.py +++ b/invenio_communities/members/services/service.py @@ -2,7 +2,7 @@ # # Copyright (C) 2022 Northwestern University. # Copyright (C) 2022-2024 CERN. -# Copyright (C) 2022-2023 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # # Invenio-Communities is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. @@ -54,7 +54,7 @@ def invite_expires_at(): """Get the invitation expiration date.""" return ( - datetime.utcnow().replace(tzinfo=timezone.utc) + datetime.now(timezone.utc) + current_app.config["COMMUNITIES_INVITATIONS_EXPIRES_IN"] ) diff --git a/tests/communities/test_components.py b/tests/communities/test_components.py index 6b24c7071..a7917a971 100644 --- a/tests/communities/test_components.py +++ b/tests/communities/test_components.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # # Invenio-Communities is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. @@ -8,7 +8,7 @@ """Test components.""" from copy import deepcopy -from datetime import datetime +from datetime import datetime, timezone import pytest from invenio_access.permissions import system_identity @@ -30,7 +30,7 @@ def comm(community_service, minimal_community, owner, location): """Create minimal public community.""" c = deepcopy(minimal_community) c["slug"] = "public-{slug}".format( - slug=str(datetime.utcnow().timestamp()).replace(".", "-") + slug=str(datetime.now(timezone.utc).timestamp()).replace(".", "-") ) community = community_service.create(data=c, identity=owner.identity) owner.refresh() @@ -43,7 +43,7 @@ def comm_restricted(community_service, minimal_community, owner, location): c = deepcopy(minimal_community) c["access"]["visibility"] = "restricted" c["slug"] = "restricted-{slug}".format( - slug=str(datetime.utcnow().timestamp()).replace(".", "-") + slug=str(datetime.now(timezone.utc).timestamp()).replace(".", "-") ) community = community_service.create(data=c, identity=owner.identity) owner.refresh() diff --git a/tests/communities/test_services.py b/tests/communities/test_services.py index 65827447b..99c9b491c 100644 --- a/tests/communities/test_services.py +++ b/tests/communities/test_services.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# Copyright (C) 2022 Graz University of Technology. +# Copyright (C) 2022-2024 Graz University of Technology. # Copyright (C) 2022 Northwestern University. # # Invenio-Communities is free software; you can redistribute it and/or modify @@ -11,7 +11,7 @@ import time import uuid from copy import deepcopy -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone import arrow import pytest @@ -37,7 +37,7 @@ def comm(community_service, minimal_community, location): """Create minimal public community.""" c = deepcopy(minimal_community) c["slug"] = "{slug}".format( - slug=str(datetime.utcnow().timestamp()).replace(".", "-") + slug=str(datetime.now(timezone.utc).timestamp()).replace(".", "-") ) return community_service.create(data=c, identity=system_identity) @@ -47,7 +47,7 @@ def comm_restricted(community_service, minimal_community, location): """Create minimal restricted community.""" c = deepcopy(minimal_community) c["slug"] = "{slug}".format( - slug=str(datetime.utcnow().timestamp()).replace(".", "-") + slug=str(datetime.now(timezone.utc).timestamp()).replace(".", "-") ) c["access"]["visibility"] = "restricted" return community_service.create(data=c, identity=system_identity) @@ -56,10 +56,10 @@ def comm_restricted(community_service, minimal_community, location): def test_search_featured(community_service, comm, db, search_clear): """Test that featured entries are indexed and returned correctly.""" data = { - "start_date": datetime.utcnow().isoformat(), + "start_date": datetime.now(timezone.utc).isoformat(), } future_data = { - "start_date": (datetime.utcnow() + timedelta(days=1)).isoformat(), + "start_date": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(), } # no featured entries -> no featured communities @@ -97,7 +97,7 @@ def test_search_featured(community_service, comm, db, search_clear): assert hits[0]["id"] == comm.data["id"] # adding past featured entry to new community. first community should show up first - data["start_date"] = (datetime.utcnow() - timedelta(days=1)).isoformat() + data["start_date"] = (datetime.now(timezone.utc) - timedelta(days=1)).isoformat() community_service.featured_create( identity=system_identity, community_id=c2.data["id"], data=data ).to_dict() @@ -110,7 +110,7 @@ def test_search_featured(community_service, comm, db, search_clear): assert hits[1]["id"] == c2.data["id"] # adding more current past featured entry to new community. new community should show up first - data["start_date"] = datetime.utcnow().isoformat() + data["start_date"] = datetime.now(timezone.utc).isoformat() community_service.featured_create( identity=system_identity, community_id=c2.data["id"], data=data ).to_dict() @@ -126,7 +126,7 @@ def test_search_featured(community_service, comm, db, search_clear): def test_reindex_featured_entries_task(community_service, comm, db, search_clear): """Test that reindexing task works.""" tomorrow = { - "start_date": (datetime.utcnow() + timedelta(days=1)).isoformat(), + "start_date": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(), } c2 = community_service.create( identity=system_identity, data={**comm.data, "slug": "c2-id"} @@ -138,7 +138,7 @@ def test_reindex_featured_entries_task(community_service, comm, db, search_clear near_future_difference = 2 near_future = { "start_date": ( - datetime.utcnow() + timedelta(seconds=near_future_difference) + datetime.now(timezone.utc) + timedelta(seconds=near_future_difference) ).isoformat(), } community_service.featured_create( @@ -165,7 +165,7 @@ def test_reindex_featured_entries_task(community_service, comm, db, search_clear def test_create_featured(community_service, comm, comm_restricted): """Test that a featured entry can be created.""" data = { - "start_date": datetime.utcnow().isoformat(), + "start_date": datetime.now(timezone.utc).isoformat(), } f = community_service.featured_create( @@ -190,10 +190,10 @@ def test_create_featured(community_service, comm, comm_restricted): def test_get_featured(community_service, comm): """Test that featured entries are indexed and returned correctly.""" data = { - "start_date": datetime.utcnow().isoformat(), + "start_date": datetime.now(timezone.utc).isoformat(), } future_data = { - "start_date": (datetime.utcnow() + timedelta(days=1)).isoformat(), + "start_date": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(), } community_service.featured_create( @@ -213,10 +213,10 @@ def test_get_featured(community_service, comm): def test_delete_featured(community_service, comm): """Test that featured entries are deleted correctly.""" data = { - "start_date": datetime.utcnow().isoformat(), + "start_date": datetime.now(timezone.utc).isoformat(), } future_data = { - "start_date": (datetime.utcnow() + timedelta(days=1)).isoformat(), + "start_date": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(), } past_entry = community_service.featured_create( @@ -260,10 +260,10 @@ def test_delete_featured(community_service, comm): def test_update_featured(community_service, comm): """Test that featured entries are indexed and returned correctly.""" data = { - "start_date": datetime.utcnow().isoformat(), + "start_date": datetime.now(timezone.utc).isoformat(), } future_data = { - "start_date": (datetime.utcnow() + timedelta(days=1)).isoformat(), + "start_date": (datetime.now(timezone.utc) + timedelta(days=1)).isoformat(), } past_entry = community_service.featured_create( @@ -427,7 +427,7 @@ def test_community_deletion(community_service, users, comm): assert tombstone.removal_reason is None assert tombstone.note == tombstone_info["note"] assert isinstance(tombstone.citation_text, str) - assert arrow.get(tombstone.removal_date).date() == datetime.utcnow().date() + assert arrow.get(tombstone.removal_date).date() == datetime.now(timezone.utc).date() # mark the community for purge community = community_service.mark_community_for_purge( diff --git a/tests/communities/test_tombstone.py b/tests/communities/test_tombstone.py index 6acc55abd..9026561f5 100644 --- a/tests/communities/test_tombstone.py +++ b/tests/communities/test_tombstone.py @@ -1,13 +1,15 @@ # -*- coding: utf-8 -*- # # Copyright (C) 2023 TU Wien. +# Copyright (C) 2024 Graz University of Technology. # # Invenio-Communities is free software; you can redistribute it and/or modify # it under the terms of the MIT License; see LICENSE file for more details. """Tests for the tombstone field and community deletion field.""" -import datetime + +from datetime import datetime, timezone import pytest from invenio_requests.resolvers.registry import ResolverRegistry @@ -39,7 +41,7 @@ def test_tombstone_creation(app): "removed_by": {"user": "1"}, "removal_reason": {"id": "spam"}, "note": "nothing in particular", - "removal_date": datetime.datetime.utcnow(), + "removal_date": datetime.now(timezone.utc), "citation_text": "No citation available, sorry", "is_visible": False, } @@ -55,7 +57,7 @@ def test_tombstone_creation(app): def test_tombstone_invalid_removed_by(app): """Test the failure of tombstone creation if the `removed_by` entry is invalid.""" - for invalid_value in [[], datetime.datetime.utcnow()]: + for invalid_value in [[], datetime.now(timezone.utc)]: with pytest.raises(ValueError): Tombstone({"removed_by": invalid_value})