Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

request: add subcommunity invitation request #1249

Merged
merged 5 commits into from
Nov 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions invenio_communities/notifications/builders.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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"]),
]
carlinmack marked this conversation as resolved.
Show resolved Hide resolved


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):
carlinmack marked this conversation as resolved.
Show resolved Hide resolved
"""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):
carlinmack marked this conversation as resolved.
Show resolved Hide resolved
"""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):
carlinmack marked this conversation as resolved.
Show resolved Hide resolved
"""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"]),
]
7 changes: 6 additions & 1 deletion invenio_communities/subcommunities/services/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
105 changes: 103 additions & 2 deletions invenio_communities/subcommunities/services/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.")
carlinmack marked this conversation as resolved.
Show resolved Hide resolved

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.

Expand All @@ -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`.
slint marked this conversation as resolved.
Show resolved Hide resolved
"""
if not app:
return
return app.config.get(
"COMMUNITIES_SUB_INVITATION_REQUEST_CLS", SubCommunityInvitationRequest
)
28 changes: 26 additions & 2 deletions invenio_communities/subcommunities/services/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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,
)
Original file line number Diff line number Diff line change
@@ -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 -%}
<table style="font-family:'Lato',Helvetica,Arial,sans-serif;border-spacing:15px">
<tr>
<td>{{ _('@{username} accepted the invitation for "{community_title}" to join as a subcommunity of "{parent_community_title}".').format(**msg_ctx) }}
</td>
</tr>
<tr>
<td><a href="{{ request_link }}" class="button">{{ _("View the request")}}</a></td>
</tr>
<tr>
<td><strong>—<strong></td>
</tr>
<tr>
<td style="font-size:smaller">{{ _("This is an auto-generated message. To manage notifications, visit your")}} <a href="{{ account_settings_link }}">{{ _("account settings")}}</a>.</td>
</tr>
</table>
{%- 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 %}
Loading
Loading