Skip to content

Commit

Permalink
community migration requests UI
Browse files Browse the repository at this point in the history
  • Loading branch information
Ducica committed Jan 16, 2025
1 parent 951ee8e commit 927a877
Show file tree
Hide file tree
Showing 8 changed files with 548 additions and 18 deletions.
6 changes: 6 additions & 0 deletions oarepo_communities/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ class CommunityAlreadyIncludedException(Exception):
description = "The record is already included in this community."


class TargetCommunityNotProvidedException(Exception):
"""Target community not provided in the migration request"""

description = "Target community not provided in the migration request."


class CommunityNotIncludedException(Exception):
"""The record is already in the community."""

Expand Down
214 changes: 205 additions & 9 deletions oarepo_communities/requests/migration.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,41 @@
from __future__ import annotations
import marshmallow as ma
from invenio_access.permissions import system_identity
from invenio_requests.proxies import current_requests_service
from invenio_requests.resolvers.registry import ResolverRegistry
from oarepo_requests.actions.generic import OARepoAcceptAction
from oarepo_requests.proxies import current_oarepo_requests_service
from oarepo_requests.types import ModelRefTypes
from oarepo_requests.types.generic import OARepoRequestType
from oarepo_requests.types.generic import NonDuplicableOARepoRequestType
from oarepo_runtime.datastreams.utils import get_record_service_for_record
from oarepo_runtime.i18n import lazy_gettext as _
from oarepo_requests.utils import (
classproperty,
is_auto_approved,
request_identity_matches,
open_request_exists,
)
from oarepo_ui.resources.components import AllowedCommunitiesComponent

from ..errors import CommunityAlreadyIncludedException

from ..errors import (
CommunityAlreadyIncludedException,
TargetCommunityNotProvidedException,
)
from ..proxies import current_oarepo_communities

from typing import TYPE_CHECKING, Any
from typing_extensions import override

if TYPE_CHECKING:
from flask_babel.speaklater import LazyString
from flask_principal import Identity
from invenio_drafts_resources.records import Record
from invenio_requests.customizations.actions import RequestAction
from invenio_requests.records.api import Request

from oarepo_requests.typing import EntityReference


class InitiateCommunityMigrationAcceptAction(OARepoAcceptAction):
"""
Expand All @@ -22,7 +46,7 @@ def apply(self, identity, request_type, topic, uow, *args, **kwargs):
creator_ref = ResolverRegistry.reference_identity(identity)
request_item = current_oarepo_requests_service.create(
system_identity,
data={"payload": self.request["payload"]},
data={"payload": self.request.get("payload", {})},
request_type=ConfirmCommunityMigrationRequestType.type_id,
topic=topic,
creator=creator_ref,
Expand All @@ -42,7 +66,9 @@ def apply(self, identity, request_type, topic, uow, *args, **kwargs):
# coordination along multiple submission like requests? can only one be available at time?
# ie.
# and what if the community is deleted before the request is processed?
community_id = self.request.receiver.resolve().community_id
community_id, role = (
self.request.receiver.resolve().entities[0]._parse_ref_dict()
)

service = get_record_service_for_record(topic)
community_inclusion_service = (
Expand All @@ -59,7 +85,7 @@ def apply(self, identity, request_type, topic, uow, *args, **kwargs):
)


class InitiateCommunityMigrationRequestType(OARepoRequestType):
class InitiateCommunityMigrationRequestType(NonDuplicableOARepoRequestType):
"""Request which is used to start migrating record from one primary community to another one.
The recipient of this request type should be the community role of the current primary community, that is the owner
of the current community must agree that the record could be migrated elsewhere.
Expand All @@ -70,12 +96,86 @@ class InitiateCommunityMigrationRequestType(OARepoRequestType):
type_id = "initiate_community_migration"
name = _("Inititiate Community migration")

description = _("Request initiation of Community migration.")

topic_can_be_none = False
allowed_topic_ref_types = ModelRefTypes(published=True, draft=True)
payload_schema = {
"community": ma.fields.String(),
"community": ma.fields.String(required=True),
}

@override
def stateful_name(
self,
identity: Identity,
*,
topic: Record,
request: Request | None = None,
**kwargs: Any,
) -> str | LazyString:
"""Return the stateful name of the request."""
if is_auto_approved(self, identity=identity, topic=topic):
return _("Inititiate Community migration")
if not request:
return _("Request community migration")
match request.status:
case "submitted":
return _("Community migration initiated")
case _:
return _("Request community migration")

@override
def stateful_description(
self,
identity: Identity,
*,
topic: Record,
request: Request | None = None,
**kwargs: Any,
) -> str | LazyString:
"""Return the stateful description of the request."""
if is_auto_approved(self, identity=identity, topic=topic):
return _(
"Click to immediately start migration. "
"After submitting the request will immediatelly be forwarded to responsible person(s) in the target community."
)

if not request:
return _(
"After you submit community migration request, it will first have to be approved by curators/owners of the current community. "
"Then it will have to be accepted by curators/owners of the target community. "
"You will be notified about the decision by email."
)
match request.status:
case "submitted":
if request_identity_matches(request.created_by, identity):
return _(
"The community migration request has been submitted. "
"You will be notified about the decision by email."
)
if request_identity_matches(request.receiver, identity):
return _(
"User has requested community migration. "
"You can now accept or decline the request."
)
return _("Community migration request has been submitted.")
case _:
if request_identity_matches(request.created_by, identity):
return _("Submit to initiate community migration. ")

return _("Request not yet submitted.")

form = {
"field": "community",
"ui_widget": "TargetCommunitySelector",
"read_only_ui_widget": "SelectedTargetCommunity",
"props": {
"requestType": "initiate_community_migration",
},
}

editable = False

@classmethod
@property
def available_actions(cls):
Expand All @@ -84,10 +184,35 @@ def available_actions(cls):
"accept": InitiateCommunityMigrationAcceptAction,
}

@classmethod
def is_applicable_to(
cls, identity: Identity, topic: Record, *args: Any, **kwargs: Any
) -> bool:
"""Check if the request type is applicable to the topic."""

if open_request_exists(topic, cls.type_id) or open_request_exists(
topic, "confirm_community_migration"
):
return False
# check if the user has more than one community to which they can migrate
allowed_communities_count = 0
for _ in AllowedCommunitiesComponent.get_allowed_communities(
identity, "create"
):
allowed_communities_count += 1
if allowed_communities_count > 1:
break

if allowed_communities_count <= 1:
return False

return super().is_applicable_to(identity, topic, *args, **kwargs)

def can_create(self, identity, data, receiver, topic, creator, *args, **kwargs):
super().can_create(identity, data, receiver, topic, creator, *args, **kwargs)
target_community_id = data["payload"]["community"]

target_community_id = data.get("payload", {}).get("community", None)
if not target_community_id:
raise TargetCommunityNotProvidedException("Target community not provided.")
already_included = target_community_id == str(
topic.parent.communities.default.id
)
Expand All @@ -97,7 +222,7 @@ def can_create(self, identity, data, receiver, topic, creator, *args, **kwargs):
)


class ConfirmCommunityMigrationRequestType(OARepoRequestType):
class ConfirmCommunityMigrationRequestType(NonDuplicableOARepoRequestType):
"""
Performs the primary community migration. The recipient of this request type should be the community
owner of the new community.
Expand All @@ -108,6 +233,77 @@ class ConfirmCommunityMigrationRequestType(OARepoRequestType):

allowed_topic_ref_types = ModelRefTypes(published=True, draft=True)

form = {
"field": "community",
"read_only_ui_widget": "SelectedTargetCommunity",
"props": {
"requestType": "initiate_community_migration",
"placeholder": _("Yes or no"),
"community": {"id": "community", "label": _("Community")},
},
}

@override
def stateful_name(
self,
identity: Identity,
*,
topic: Record,
request: Request | None = None,
**kwargs: Any,
) -> str | LazyString:
"""Return the stateful name of the request."""
# if is_auto_approved(self, identity=identity, topic=topic):
# return _("Migrate record")

if not request:
return _("Confirm community migration")
match request.status:
case "submitted":
return _("Community migration confirmation pending")
case _:
return _("Confirm community migration")

@override
def stateful_description(
self,
identity: Identity,
*,
topic: Record,
request: Request | None = None,
**kwargs: Any,
) -> str | LazyString:
"""Return the stateful description of the request."""
# if is_auto_approved(self, identity=identity, topic=topic):
# return _(
# "Click to immediately migrate record to a different community. "
# "After submitting the record will immediatelly be migrated to another community."
# )

if not request:
return _(
"Confirm the migration of the record to the new primary community. "
"This request must be accepted by the curators/owners of the new community."
)

match request.status:
case "submitted":
if request_identity_matches(request.created_by, identity):
return _(
"The confirmation request has been submitted to the target community. "
"You will be notified about their decision by email."
)
if request_identity_matches(request.receiver, identity):
return _(
"A request to confirm the community migration has been received. "
"You can now accept or decline the request."
)
return _("The community migration confirmation request is pending.")
case _:
if request_identity_matches(request.created_by, identity):
return _("Submit to confirm the community migration.")
return _("Request not yet submitted.")

@classmethod
@property
def available_actions(cls):
Expand Down
37 changes: 30 additions & 7 deletions oarepo_communities/requests/submission_secondary.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
from oarepo_requests.actions.generic import OARepoAcceptAction
from oarepo_requests.types import ModelRefTypes
from oarepo_requests.types.generic import OARepoRequestType
from oarepo_requests.types.generic import NonDuplicableOARepoRequestType
from oarepo_runtime.datastreams.utils import get_record_service_for_record
from oarepo_runtime.i18n import lazy_gettext as _
import marshmallow as ma

from ..errors import CommunityAlreadyIncludedException
from ..errors import (
CommunityAlreadyIncludedException,
TargetCommunityNotProvidedException,
)
from ..proxies import current_oarepo_communities


class CommunitySubmissionAcceptAction(OARepoAcceptAction):

def apply(self, identity, request_type, topic, uow, *args, **kwargs):

community_id = self.request.receiver.resolve().community_id
community_id, role = (
self.request.receiver.resolve().entities[0]._parse_ref_dict()
)
service = get_record_service_for_record(topic)
community_inclusion_service = (
current_oarepo_communities.community_inclusion_service
Expand All @@ -22,12 +26,13 @@ def apply(self, identity, request_type, topic, uow, *args, **kwargs):
)


class SecondaryCommunitySubmissionRequestType(OARepoRequestType):
class SecondaryCommunitySubmissionRequestType(NonDuplicableOARepoRequestType):
"""Review request for submitting a record to a community."""

type_id = "secondary_community_submission"
name = _("Secondary community submission")
allowed_topic_ref_types = ModelRefTypes(published=True, draft=True)
editable = False

@classmethod
@property
Expand All @@ -37,9 +42,27 @@ def available_actions(cls):
"accept": CommunitySubmissionAcceptAction,
}

topic_can_be_none = False
payload_schema = {
"community": ma.fields.String(required=True),
}

form = {
"field": "community",
"ui_widget": "SecondaryCommunitySelector",
"read_only_ui_widget": "SelectedTargetCommunity",
"props": {
"requestType": "secondary_community_submission",
},
}

def can_create(self, identity, data, receiver, topic, creator, *args, **kwargs):
print("can_create", flush=True)
super().can_create(identity, data, receiver, topic, creator, *args, **kwargs)
target_community_id = data["payload"]["community"]
target_community_id = data.get("payload", {}).get("community", None)
print("target_community_id", target_community_id, flush=True)
if not target_community_id:
raise TargetCommunityNotProvidedException("Target community not provided.")

already_included = target_community_id in topic.parent.communities.ids
if already_included:
Expand Down
3 changes: 2 additions & 1 deletion oarepo_communities/services/permissions/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from oarepo_communities.errors import (
MissingCommunitiesError,
MissingDefaultCommunityError,
TargetCommunityNotProvidedException,
)
from oarepo_communities.proxies import current_oarepo_communities

Expand Down Expand Up @@ -213,7 +214,7 @@ def _get_data_communities(self, data=None, **kwargs):
try:
community_id = data["payload"]["community"]
except KeyError:
raise MissingDefaultCommunityError(
raise TargetCommunityNotProvidedException(
"Community not defined in request payload."
)
return [community_id]
Expand Down
Loading

0 comments on commit 927a877

Please sign in to comment.