diff --git a/invenio_communities/notifications/builders.py b/invenio_communities/notifications/builders.py
index 39a40672e..4e5a293f3 100644
--- a/invenio_communities/notifications/builders.py
+++ b/invenio_communities/notifications/builders.py
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
#
+# Copyright (C) 2024 CERN.
# Copyright (C) 2023 Graz University of Technology.
#
# Invenio-Communities is free software; you can redistribute it and/or modify
@@ -216,3 +217,78 @@ class SubCommunityDecline(SubCommunityBuilderBase):
"""Notification builder for subcommunity request decline."""
type = f"{SubCommunityBuilderBase.type}.decline"
+
+
+class SubComInvitationBuilderBase(SubCommunityBuilderBase):
+ """Base notification builder for subcommunity invitation requests."""
+
+ type = "subcommunity-invitation-request"
+
+ context = [
+ EntityResolve("request"),
+ EntityResolve("request.created_by"),
+ EntityResolve("request.receiver"),
+ EntityResolve("executing_user"),
+ ]
+
+ recipients = [
+ CommunityMembersRecipient("request.created_by", roles=["owner", "manager"]),
+ CommunityMembersRecipient("request.receiver", roles=["owner", "manager"]),
+ ]
+
+
+class SubComInvitationCreate(SubComInvitationBuilderBase):
+ """Notification builder for subcommunity request creation."""
+
+ type = f"{SubComInvitationBuilderBase.type}.create"
+
+ context = [
+ EntityResolve("request"),
+ EntityResolve("request.created_by"),
+ EntityResolve("request.receiver"),
+ # EntityResolve("executing_user") creating via script only for now
+ ]
+
+ recipients = [
+ CommunityMembersRecipient("request.receiver", roles=["owner", "manager"]),
+ ]
+
+
+class SubComInvitationAccept(SubComInvitationBuilderBase):
+ """Notification builder for subcommunity request accept."""
+
+ type = f"{SubComInvitationBuilderBase.type}.accept"
+
+ recipient_filters = [
+ UserPreferencesRecipientFilter(),
+ # Don't send notifications to the user performing the action
+ UserRecipientFilter("executing_user"),
+ ]
+
+
+class SubComInvitationDecline(SubComInvitationBuilderBase):
+ """Notification builder for subcommunity request decline."""
+
+ type = f"{SubComInvitationBuilderBase.type}.decline"
+
+ recipient_filters = [
+ UserPreferencesRecipientFilter(),
+ # Don't send notifications to the user performing the action
+ UserRecipientFilter("executing_user"),
+ ]
+
+
+class SubComInvitationExpire(SubComInvitationBuilderBase):
+ """Notification builder for subcommunity invitation expire."""
+
+ type = f"{SubComInvitationBuilderBase.type}.expire"
+
+ context = [
+ EntityResolve("request"),
+ EntityResolve("request.created_by"),
+ EntityResolve("request.receiver"),
+ ]
+
+ recipients = [
+ CommunityMembersRecipient("request.receiver", roles=["owner", "manager"]),
+ ]
diff --git a/invenio_communities/subcommunities/services/__init__.py b/invenio_communities/subcommunities/services/__init__.py
index 0543e7479..939e2a188 100644
--- a/invenio_communities/subcommunities/services/__init__.py
+++ b/invenio_communities/subcommunities/services/__init__.py
@@ -7,6 +7,11 @@
"""Subcommunities module service."""
from .config import SubCommunityServiceConfig
+from .request import SubCommunityInvitationRequest
from .service import SubCommunityService
-__all__ = ("SubCommunityService", "SubCommunityServiceConfig")
+__all__ = (
+ "SubCommunityService",
+ "SubCommunityServiceConfig",
+ "SubCommunityInvitationRequest",
+)
diff --git a/invenio_communities/subcommunities/services/request.py b/invenio_communities/subcommunities/services/request.py
index 6c2464342..c4ed86bf2 100644
--- a/invenio_communities/subcommunities/services/request.py
+++ b/invenio_communities/subcommunities/services/request.py
@@ -10,6 +10,7 @@
from invenio_i18n import lazy_gettext as _
from invenio_notifications.services.uow import NotificationOp
from invenio_requests.customizations import RequestType, actions
+from marshmallow.exceptions import ValidationError
import invenio_communities.notifications.builders as notifications
from invenio_communities.proxies import current_communities
@@ -52,10 +53,10 @@ def execute(self, identity, uow):
class SubCommunityRequest(RequestType):
- """Request to add a subcommunity to a community."""
+ """Request to join a parent community as a subcommunity."""
type_id = "subcommunity"
- name = _("Subcommunity Request")
+ name = _("Subcommunity request")
creator_can_be_none = False
topic_can_be_none = False
@@ -80,6 +81,91 @@ class SubCommunityRequest(RequestType):
}
+class CreateSubcommunityInvitation(actions.CreateAndSubmitAction):
+ """Represents an accept action used to accept a subcommunity."""
+
+ def execute(self, identity, uow):
+ """Execute approve action."""
+ parent = self.request.created_by.resolve()
+ if not parent.children.allow:
+ raise ValidationError("Assigned parent is not allowed to be a parent.")
+
+ uow.register(
+ NotificationOp(
+ notifications.SubComInvitationCreate.build(
+ identity=identity, request=self.request
+ )
+ )
+ )
+
+ super().execute(identity, uow)
+
+
+class AcceptSubcommunityInvitation(actions.AcceptAction):
+ """Represents an accept action used to accept a subcommunity."""
+
+ def execute(self, identity, uow):
+ """Execute approve action."""
+ child = self.request.receiver.resolve().id
+ parent = self.request.created_by.resolve().id
+ current_communities.service.bulk_update_parent(
+ system_identity, [child], parent_id=parent, uow=uow
+ )
+ uow.register(
+ NotificationOp(
+ notifications.SubComInvitationAccept.build(
+ identity=identity, request=self.request
+ )
+ )
+ )
+ super().execute(identity, uow)
+
+
+class DeclineSubcommunityInvitation(actions.DeclineAction):
+ """Represents a decline action used to decline a subcommunity."""
+
+ def execute(self, identity, uow):
+ """Execute decline action."""
+ # We override just to send a notification
+ uow.register(
+ NotificationOp(
+ notifications.SubComInvitationDecline.build(
+ identity=identity, request=self.request
+ )
+ )
+ )
+ super().execute(identity, uow)
+
+
+class SubCommunityInvitationRequest(RequestType):
+ """Request from a parent community to community to join."""
+
+ type_id = "subcommunity-invitation"
+ name = _("Subcommunity invitation")
+
+ creator_can_be_none = False
+ topic_can_be_none = True
+ allowed_creator_ref_types = ["community"]
+ allowed_receiver_ref_types = ["community"]
+ allowed_topic_ref_types = ["community"]
+
+ available_actions = {
+ "delete": actions.DeleteAction,
+ "cancel": actions.CancelAction,
+ # Custom implemented actions
+ "create": CreateSubcommunityInvitation,
+ "accept": AcceptSubcommunityInvitation,
+ "decline": DeclineSubcommunityInvitation,
+ }
+
+ needs_context = {
+ "community_roles": [
+ "owner",
+ "manager",
+ ]
+ }
+
+
def subcommunity_request_type(app):
"""Return the subcommunity request type.
@@ -91,3 +177,18 @@ def subcommunity_request_type(app):
if not app:
return
return app.config.get("COMMUNITIES_SUB_REQUEST_CLS", SubCommunityRequest)
+
+
+def subcommunity_invitation_request_type(app):
+ """Return the subcommunity request type.
+
+ Since it can be overridden by the application, this function should be used
+ as the entry point for the request type.
+
+ It must return a class that inherits from `RequestType`.
+ """
+ if not app:
+ return
+ return app.config.get(
+ "COMMUNITIES_SUB_INVITATION_REQUEST_CLS", SubCommunityInvitationRequest
+ )
diff --git a/invenio_communities/subcommunities/services/service.py b/invenio_communities/subcommunities/services/service.py
index bd15acb94..3d5a7eaa6 100644
--- a/invenio_communities/subcommunities/services/service.py
+++ b/invenio_communities/subcommunities/services/service.py
@@ -6,20 +6,22 @@
# it under the terms of the MIT License; see LICENSE file for more details.
"""Subcommunities service."""
+from datetime import datetime, timedelta, timezone
+
from invenio_i18n import gettext as _
from invenio_notifications.services.uow import NotificationOp
from invenio_records_resources.services.base import LinksTemplate, Service
-from invenio_records_resources.services.records.links import pagination_links
from invenio_records_resources.services.records.schema import ServiceSchemaWrapper
from invenio_records_resources.services.uow import unit_of_work
+from invenio_requests import current_request_type_registry
from invenio_requests.proxies import current_requests_service as requests_service
-from invenio_search.engine import dsl
from werkzeug.local import LocalProxy
import invenio_communities.notifications.builders as notifications
from invenio_communities.proxies import current_communities, current_roles
from .errors import ParentChildrenNotAllowed
+from .request import SubCommunityInvitationRequest
community_service = LocalProxy(lambda: current_communities.service)
@@ -150,3 +152,25 @@ def join(self, identity, id_, data, uow=None):
# expandable_fields=self.expandable_fields,
# expand=True,
# )
+
+ @unit_of_work()
+ def create_subcommunity_invitation_request(
+ self, identity, parent_community_id, child_community_id, data, uow=None
+ ):
+ """Create and submit a SubCommunityInvitation request."""
+ type_ = current_request_type_registry.lookup(
+ SubCommunityInvitationRequest.type_id
+ )
+ parent_community = community_service.record_cls.pid.resolve(parent_community_id)
+ child_community = community_service.record_cls.pid.resolve(child_community_id)
+ expires_at = datetime.now(timezone.utc) + timedelta(days=30)
+
+ requests_service.create(
+ identity=identity,
+ data=data,
+ request_type=type_,
+ creator=parent_community,
+ receiver=child_community,
+ expires_at=expires_at,
+ uow=uow,
+ )
diff --git a/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.accept.jinja b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.accept.jinja
new file mode 100644
index 000000000..a350fff8a
--- /dev/null
+++ b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.accept.jinja
@@ -0,0 +1,59 @@
+{% set request = notification.context.request %}
+{% set parent_community = request.created_by %}
+{% set community = request.receiver %}
+{% set user = notification.context.executing_user %}
+{% set ui = config.SITE_UI_URL %}
+{% set msg_ctx = {
+ "community_id": community.slug,
+ "community_title": community.metadata.title,
+ "parent_community_id": parent_community.slug,
+ "parent_community_title": parent_community.metadata.title,
+ "request_id": request.id,
+ "username": user.username or user.profile.full_name,
+ "ui": ui,
+} %}
+
+{# TODO: use request.links.self_html when issue issue is resolved: https://github.com/inveniosoftware/invenio-rdm-records/issues/1327 #}
+{% set request_link = "{ui}/communities/{community_id}/requests/{request_id}".format(**msg_ctx) %}
+{% set account_settings_link = "{ui}/account/settings/notifications".format(**msg_ctx) %}
+
+{%- block subject -%}
+{{ _('✅ Invitation accepted for {community_title} to join the {parent_community_title} community').format(**msg_ctx) }}
+{%- endblock subject -%}
+
+{%- block html_body -%}
+
+
+ {{ _('@{username} accepted the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+ |
+
+
+ {{ _("View the request")}} |
+
+
+ — |
+
+
+ {{ _("This is an auto-generated message. To manage notifications, visit your")}} {{ _("account settings")}}. |
+
+
+{%- endblock html_body %}
+
+{%- block plain_body -%}
+{{ _('@{username} accepted the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+
+{{ _("View the request at:") }} {{ request_link }}
+
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your account settings at: ") }}{{ account_settings_link }}
+{%- endblock plain_body %}
+
+{# Markdown for Slack/Mattermost/chat #}
+{%- block md_body -%}
+{{ _('@{username} accepted the invitation the request for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+
+[{{ _("View the request") }}]({{ request_link }})
+
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your") }} [{{ _("account settings") }}]({{ account_settings_link }}).
+{%- endblock md_body %}
diff --git a/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.create.jinja b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.create.jinja
new file mode 100644
index 000000000..bd60c3ff2
--- /dev/null
+++ b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.create.jinja
@@ -0,0 +1,63 @@
+{% set request = notification.context.request %}
+{% set parent_community = request.created_by %}
+{% set community = request.receiver %}
+{% set ui = config.SITE_UI_URL %}
+{% set msg_ctx = {
+ "community_id": community.slug,
+ "community_title": community.metadata.title,
+ "parent_community_id": parent_community.slug,
+ "parent_community_title": parent_community.metadata.title,
+ "request_id": request.id,
+ "expires_at": request.expires_at | from_isodatetime | dateformat(format="long"),
+ "ui": ui,
+} %}
+
+{# TODO: use request.links.self_html when issue issue is resolved: https://github.com/inveniosoftware/invenio-rdm-records/issues/1327 #}
+{% set request_link = "{ui}/communities/{community_id}/requests/{request_id}".format(**msg_ctx) %}
+{% set account_settings_link = "{ui}/account/settings/notifications".format(**msg_ctx) %}
+
+{%- block subject -%}
+{{ _('📬 Invitation to join the {parent_community_title} community').format(**msg_ctx) }}
+{%- endblock subject -%}
+
+{%- block html_body -%}
+
+
+ {{ _('We would like to invite your community "{community_title}" to join as a subcommunity of "{parent_community_title}"').format(**msg_ctx) }}
+ |
+
+
+ {{ _("View the request")}} |
+
+
+ {{ _('This invitation will expire on {expires_at}.').format(**msg_ctx) }} |
+
+
+ — |
+
+
+ {{ _("This is an auto-generated message. To manage notifications, visit your")}} {{ _("account settings")}}. |
+
+
+{%- endblock html_body %}
+
+{%- block plain_body -%}
+{{ _('We would like to invite your community "{community_title}" to join as a subcommunity of "{parent_community_title}"').format(**msg_ctx) }}
+
+{{ _("View the request at:") }} {{ request_link }}
+
+{{ _('This invitation will expire on {expires_at}.').format(**msg_ctx) }}
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your account settings at: ") }}{{ account_settings_link }}
+{%- endblock plain_body %}
+
+{# Markdown for Slack/Mattermost/chat #}
+{%- block md_body -%}
+{{ _('The community *{community_title}* has requested to join as a subcommunity of *{parent_community_title}*').format(**msg_ctx) }}
+
+[{{ _("View the request") }}]({{ request_link }})
+
+{{ _('This invitation will expire on {expires_at}.').format(**msg_ctx) }}
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your") }} [{{ _("account settings") }}]({{ account_settings_link }}).
+{%- endblock md_body %}
diff --git a/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.decline.jinja b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.decline.jinja
new file mode 100644
index 000000000..c472cf1f9
--- /dev/null
+++ b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.decline.jinja
@@ -0,0 +1,59 @@
+{% set request = notification.context.request %}
+{% set parent_community = request.created_by %}
+{% set community = request.receiver %}
+{% set user = notification.context.executing_user %}
+{% set ui = config.SITE_UI_URL %}
+{% set msg_ctx = {
+ "community_id": community.slug,
+ "community_title": community.metadata.title,
+ "parent_community_id": parent_community.slug,
+ "parent_community_title": parent_community.metadata.title,
+ "request_id": request.id,
+ "username": user.username or user.profile.full_name,
+ "ui": ui,
+} %}
+
+{# TODO: use request.links.self_html when issue issue is resolved: https://github.com/inveniosoftware/invenio-rdm-records/issues/1327 #}
+{% set request_link = "{ui}/communities/{community_id}/requests/{request_id}".format(**msg_ctx) %}
+{% set account_settings_link = "{ui}/account/settings/notifications".format(**msg_ctx) %}
+
+{%- block subject -%}
+{{ _('⛔️ Subcommunity invitation declined for {community_title} to join the {parent_community_title} community').format(**msg_ctx) }}
+{%- endblock subject -%}
+
+{%- block html_body -%}
+
+
+ {{ _('@{username} declined the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+ |
+
+
+ {{ _("View the request")}} |
+
+
+ — |
+
+
+ {{ _("This is an auto-generated message. To manage notifications, visit your")}} {{ _("account settings")}}. |
+
+
+{%- endblock html_body %}
+
+{%- block plain_body -%}
+{{ _('@{username} declined the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+
+{{ _("View the request at:") }} {{ request_link }}
+
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your account settings at: ") }}{{ account_settings_link }}
+{%- endblock plain_body %}
+
+{# Markdown for Slack/Mattermost/chat #}
+{%- block md_body -%}
+{{ _('@{username} declined the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
+
+[{{ _("View the request") }}]({{ request_link }})
+
+—
+{{ _("This is an auto-generated message. To manage notifications, visit your") }} [{{ _("account settings") }}]({{ account_settings_link }}).
+{%- endblock md_body %}
diff --git a/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.expire.jinja b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.expire.jinja
new file mode 100644
index 000000000..9fc3a0f11
--- /dev/null
+++ b/invenio_communities/templates/semantic-ui/invenio_notifications/subcommunity-invitation-request.expire.jinja
@@ -0,0 +1,49 @@
+{% set request = notification.context.request %}
+{% set parent_community = request.created_by %}
+{% set community = request.receiver %}
+{% set ui = config.SITE_UI_URL %}
+{% set msg_ctx = {
+ "community_id": community.slug,
+ "community_title": community.metadata.title,
+ "parent_community_title": parent_community.metadata.title,
+ "request_id": request.id,
+ "ui": ui,
+} %}
+
+{# TODO: use request.links.self_html when issue issue is resolved: https://github.com/inveniosoftware/invenio-rdm-records/issues/1327 #}
+{% set request_link = "{ui}/communities/{community_id}/requests/{request_id}".format(**msg_ctx) %}
+{% set account_settings_link = "{ui}/account/settings/notifications".format(**msg_ctx) %}
+
+{%- block subject -%}
+ {{ _("⌛️ The invitation for {community_title} to join the '{parent_community_title}' community expired").format(**msg_ctx) }}
+{%- endblock subject -%}
+
+{%- block html_body -%}
+
+
+ {{ _("The invitation for '{community_title}' to join the '{parent_community_title}' community has expired.").format(**msg_ctx) }} |
+
+
+ {{ _("Check out the invitation")}} |
+
+
+ _ |
+
+
+ {{ _("This is an auto-generated message. To manage notifications, visit your")}} {{ _("account settings")}}. |
+
+
+{%- endblock html_body %}
+
+{%- block plain_body -%}
+{{ _("The invitation for {community_title} to join the '{parent_community_title}' community has expired.").format(**msg_ctx) }}
+
+{{ _("Check out the invitation:") }} {{ request_link }}
+{%- endblock plain_body %}
+
+{# Markdown for Slack/Mattermost/chat #}
+{%- block md_body -%}
+{{ _("The invitation for *{community_title}* to join community *{parent_community_title}* has expired.").format(**msg_ctx) }}
+
+[{{ _("Check out the invitation") }}]({{ request_link }})
+{%- endblock md_body %}
diff --git a/setup.cfg b/setup.cfg
index ca550b1b1..77e02c2b3 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -83,6 +83,7 @@ invenio_requests.entity_resolvers =
invenio_requests.types =
community_invitation = invenio_communities.members.services.request:CommunityInvitation
subcommunity = invenio_communities.subcommunities.services.request:subcommunity_request_type
+ subcommunity_invitation = invenio_communities.subcommunities.services.request:subcommunity_invitation_request_type
membership_request_request_type = invenio_communities.members.services.request:MembershipRequestRequestType
invenio_i18n.translations =
messages = invenio_communities