diff --git a/invenio_communities/config.py b/invenio_communities/config.py index e654bbc32..55991eb44 100644 --- a/invenio_communities/config.py +++ b/invenio_communities/config.py @@ -228,8 +228,11 @@ } """Available membership requests sort options.""" -COMMUNITIES_INVITATIONS_EXPIRES_IN = timedelta(days=30) -""""Default amount of time before an invitation expires.""" +COMMUNITIES_MEMBER_REQUESTS_EXPIRE_IN = timedelta(days=30) +""""Default amount of time before a member request expires. + +Replaces COMMUNITIES_INVITATIONS_EXPIRES_IN . +""" COMMUNITIES_LOGO_MAX_FILE_SIZE = 10**6 """Community logo size quota, in bytes.""" diff --git a/invenio_communities/members/services/request.py b/invenio_communities/members/services/request.py index 0f73b5650..4ed4d7e59 100644 --- a/invenio_communities/members/services/request.py +++ b/invenio_communities/members/services/request.py @@ -156,7 +156,17 @@ def execute(self, identity, uow): super().execute(identity, uow) -# TODO: Expiration flow: ExpireAction +class ExpireMembershipRequestAction(actions.ExpireAction): + """Expire membership request action. + + Triggered by task in invenio-requests. + """ + + def execute(self, identity, uow): + """Execute action.""" + service().close_member_request(system_identity, self.request.id, uow=uow) + # TODO: Notification flow: Investigate notifications + super().execute(identity, uow) class AcceptMembershipRequestAction(actions.AcceptAction): @@ -181,6 +191,7 @@ class MembershipRequestRequestType(RequestType): "cancel": CancelMembershipRequestAction, "accept": AcceptMembershipRequestAction, "decline": DeclineMembershipRequestAction, + "expire": ExpireMembershipRequestAction, } creator_can_be_none = False diff --git a/invenio_communities/members/services/schemas.py b/invenio_communities/members/services/schemas.py index bca6e795c..8807215b0 100644 --- a/invenio_communities/members/services/schemas.py +++ b/invenio_communities/members/services/schemas.py @@ -247,6 +247,4 @@ def get_permissions(self, obj): community_id=obj.community_id, member=obj, ) - return { - "can_update_role": is_open and can_update - } + return {"can_update_role": is_open and can_update} diff --git a/invenio_communities/members/services/service.py b/invenio_communities/members/services/service.py index 8f00b5bdd..a53967b85 100644 --- a/invenio_communities/members/services/service.py +++ b/invenio_communities/members/services/service.py @@ -10,6 +10,7 @@ """Members service.""" from datetime import datetime, timezone +from warnings import warn from flask import current_app from invenio_access.permissions import system_identity @@ -52,12 +53,18 @@ ) -def invite_expires_at(): - """Get the invitation expiration date.""" - return ( - datetime.utcnow().replace(tzinfo=timezone.utc) - + current_app.config["COMMUNITIES_INVITATIONS_EXPIRES_IN"] - ) +def member_request_expires_at(): + """Get the request expiration date.""" + expires_in_delta = current_app.config.get("COMMUNITIES_INVITATIONS_EXPIRES_IN") + if expires_in_delta: + # Deprecated + # TODO: Remove deprecation warning in v14 + warn( + "COMMUNITIES_INVITATIONS_EXPIRES_IN is deprecated. Use COMMUNITIES_MEMBER_REQUESTS_EXPIRE_IN instead.", # noqa + DeprecationWarning, + ) + expires_in_delta = current_app.config["COMMUNITIES_MEMBER_REQUESTS_EXPIRE_IN"] + return datetime.now(tz=timezone.utc) + expires_in_delta class MemberService(RecordService): @@ -712,7 +719,7 @@ def _invite_factory(self, identity, community, role, visible, member, message, u # TODO: perhaps topic should be the actual membership record # instead topic=community, - expires_at=invite_expires_at(), + expires_at=member_request_expires_at(), uow=uow, ) @@ -811,14 +818,12 @@ def request_membership(self, identity, community_id, data, uow=None): identity, data={ "title": title, - # "description": description, }, request_type=MembershipRequestRequestType, receiver=community, creator={"user": str(identity.user.id)}, topic=community, # user instead? - # TODO: Expiration flow: Consider expiration - # expires_at=invite_expires_at(), + expires_at=member_request_expires_at(), uow=uow, ) diff --git a/tests/members/test_members_resource.py b/tests/members/test_members_resource.py index 3b89610fc..35ddf86a5 100644 --- a/tests/members/test_members_resource.py +++ b/tests/members/test_members_resource.py @@ -467,6 +467,7 @@ def test_get_membership_requests( assert "id" in hit["request"] assert "status" in hit["request"] assert "expires_at" in hit["request"] + assert hit["request"]["expires_at"] is not None # hits > hit > links request_id = hit["request"]["id"] expected_links = { @@ -478,6 +479,3 @@ def test_get_membership_requests( assert expected_links == hit["links"] # hits > hit > permissions assert hit["permissions"]["can_update_role"] is True - - # TODO: Expiration flow : assess if expiration makes sense for membership requests. - # assert hit["request"]["expires_at"] is not None diff --git a/tests/members/test_members_services.py b/tests/members/test_members_services.py index b3e5eef2c..7283ec909 100644 --- a/tests/members/test_members_services.py +++ b/tests/members/test_members_services.py @@ -1345,7 +1345,7 @@ def test_request_membership_accept_flow( request = requests_service.execute_action( owner.identity, membership_request.id, "accept" ).to_dict() - ArchivedInvitation.index.refresh() # switch name? + ArchivedInvitation.index.refresh() Member.index.refresh() # Postconditions @@ -1362,6 +1362,48 @@ def test_request_membership_accept_flow( assert hit["request"]["is_open"] is False +def test_request_membership_expire_flow( + member_service, + community, + owner, + create_user, + requests_service, + db, + clean_index, +): + # Create membership request + user = create_user() + data = { + "message": "Can I join the club?", + } + community_uuid = community._record.id + membership_request = member_service.request_membership( + user.identity, + community_uuid, + data, + ) + + # Expire request + request = requests_service.execute_action( + system_identity, membership_request.id, "expire" + ).to_dict() + ArchivedInvitation.index.refresh() + Member.index.refresh() + + # Postconditions + # 1 member, the owner + res = member_service.search(owner.identity, community_uuid) + assert 1 == res.to_dict()["hits"]["total"] + + # 1 "request", the expired membership request + res = member_service.search_membership_requests(owner.identity, community_uuid) + hits = res.to_dict()["hits"] + assert 1 == hits["total"] + hit = hits["hits"][0] + assert "expired" == hit["request"]["status"] + assert hit["request"]["is_open"] is False + + # # Change notifications #