Skip to content

Commit

Permalink
Merge pull request #107 from oarepo/krist/be-443-configure-notifications
Browse files Browse the repository at this point in the history
Krist/be 443 configure notifications
  • Loading branch information
mesemus authored Jan 31, 2025
2 parents 676561b + 75af682 commit 3e6853e
Show file tree
Hide file tree
Showing 52 changed files with 1,216 additions and 1,438 deletions.
56 changes: 54 additions & 2 deletions oarepo_requests/actions/delete_published_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,32 @@

from typing import TYPE_CHECKING, Any, override

from ..notifications.builders.delete_published_record import (
DeletePublishedRecordRequestAcceptNotificationBuilder,
DeletePublishedRecordRequestSubmitNotificationBuilder,
)
from .cascade_events import cancel_requests_on_topic_delete
from .generic import OARepoAcceptAction, OARepoDeclineAction, OARepoSubmitAction

if TYPE_CHECKING:
from flask_principal import Identity
from invenio_drafts_resources.records import Record
from invenio_requests.customizations import RequestType

from typing import TYPE_CHECKING, Any

from invenio_notifications.services.uow import NotificationOp
from invenio_records_resources.services.uow import UnitOfWork
from oarepo_runtime.datastreams.utils import get_record_service_for_record
from oarepo_runtime.i18n import lazy_gettext as _

from .cascade_events import cancel_requests_on_topic_delete
from .generic import OARepoAcceptAction, OARepoDeclineAction
from .generic import OARepoAcceptAction, OARepoDeclineAction, OARepoSubmitAction

if TYPE_CHECKING:
from flask_principal import Identity
from invenio_drafts_resources.records import Record
from invenio_requests.customizations import RequestType


if TYPE_CHECKING:
from flask_principal import Identity
Expand All @@ -24,6 +45,30 @@
from invenio_requests.customizations import RequestType


class DeletePublishedRecordSubmitAction(OARepoSubmitAction):
"""Submit action for publishing draft requests."""

def apply(
self,
identity: Identity,
request_type: RequestType,
topic: Record,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> Record:
"""Publish the draft."""

uow.register(
NotificationOp(
DeletePublishedRecordRequestSubmitNotificationBuilder.build(
request=self.request
)
)
)
return super().apply(identity, request_type, topic, uow, *args, **kwargs)


class DeletePublishedRecordAcceptAction(OARepoAcceptAction):
"""Accept request for deletion of a published record and delete the record."""

Expand All @@ -43,6 +88,13 @@ def apply(
if not topic_service:
raise KeyError(f"topic {topic} service not found")
topic_service.delete(identity, topic["id"], *args, uow=uow, **kwargs)
uow.register(
NotificationOp(
DeletePublishedRecordRequestAcceptNotificationBuilder.build(
request=self.request
)
)
)
cancel_requests_on_topic_delete(self.request, topic, uow)


Expand Down
3 changes: 2 additions & 1 deletion oarepo_requests/actions/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
from functools import cached_property
from typing import TYPE_CHECKING, Any

from invenio_pidstore.errors import PersistentIdentifierError
from invenio_requests.customizations import actions
from oarepo_runtime.i18n import lazy_gettext as _

from oarepo_requests.proxies import current_oarepo_requests
from invenio_pidstore.errors import PersistentIdentifierError

if TYPE_CHECKING:
from flask_babel.speaklater import LazyString
from flask_principal import Identity
Expand Down
28 changes: 28 additions & 0 deletions oarepo_requests/actions/publish_draft.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
from typing import TYPE_CHECKING, Any

from invenio_access.permissions import system_identity
from invenio_notifications.services.uow import NotificationOp
from invenio_records_resources.services.uow import RecordCommitOp, UnitOfWork
from oarepo_runtime.datastreams.utils import get_record_service_for_record
from oarepo_runtime.i18n import lazy_gettext as _

from ..notifications.builders.publish import (
PublishDraftRequestAcceptNotificationBuilder,
PublishDraftRequestSubmitNotificationBuilder,
)
from .cascade_events import update_topic
from .generic import (
AddTopicLinksOnPayloadMixin,
Expand Down Expand Up @@ -52,6 +57,24 @@ def can_execute(self: RequestAction) -> bool:
class PublishDraftSubmitAction(PublishMixin, OARepoSubmitAction):
"""Submit action for publishing draft requests."""

def apply(
self,
identity: Identity,
request_type: RequestType,
topic: Record,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> Record:
"""Publish the draft."""

uow.register(
NotificationOp(
PublishDraftRequestSubmitNotificationBuilder.build(request=self.request)
)
)
return super().apply(identity, request_type, topic, uow, *args, **kwargs)


class PublishDraftAcceptAction(
PublishMixin, AddTopicLinksOnPayloadMixin, OARepoAcceptAction
Expand Down Expand Up @@ -86,6 +109,11 @@ def apply(
identity, id_, *args, uow=uow, expand=False, **kwargs
)
update_topic(self.request, topic, published_topic._record, uow)
uow.register(
NotificationOp(
PublishDraftRequestAcceptNotificationBuilder.build(request=self.request)
)
)
return super().apply(
identity, request_type, published_topic, uow, *args, **kwargs
)
Expand Down
9 changes: 9 additions & 0 deletions oarepo_requests/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
RequestIdentityComponent,
WorkflowTransitionComponent,
)
from oarepo_requests.notifications.generators import (
GroupEmailRecipient,
UserEmailRecipient,
)
from oarepo_requests.resolvers.ui import (
AutoApproveUIEntityResolver,
FallbackEntityReferenceUIResolver,
Expand Down Expand Up @@ -96,3 +100,8 @@
RequestIdentityComponent,
],
}

NOTIFICATION_RECIPIENTS_RESOLVERS = {
"user": {"email": UserEmailRecipient},
"group": {"email": GroupEmailRecipient},
}
15 changes: 13 additions & 2 deletions oarepo_requests/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import TYPE_CHECKING, Callable

import importlib_metadata
from deepmerge import conservative_merger
from invenio_base.utils import obj_or_import_string
from invenio_requests.proxies import current_events_service

Expand Down Expand Up @@ -97,8 +98,7 @@ def default_request_receiver(
:param creator: Creator of the request.
:param data: Payload of the request.
"""
# TODO: if the topic is one of the workflow topics, use the workflow to determine the receiver
# otherwise use the default receiver

return obj_or_import_string(
self.app.config["OAREPO_REQUESTS_DEFAULT_RECEIVER"]
)(
Expand Down Expand Up @@ -214,6 +214,13 @@ def init_config(self, app: Flask) -> None:
if event_type not in app_registered_event_types:
app_registered_event_types.append(event_type)

app_registered_event_types = app.config.setdefault(
"NOTIFICATION_RECIPIENTS_RESOLVERS", {}
)
app.config["NOTIFICATION_RECIPIENTS_RESOLVERS"] = conservative_merger.merge(
app_registered_event_types, config.NOTIFICATION_RECIPIENTS_RESOLVERS
)


def api_finalize_app(app: Flask) -> None:
"""Finalize app."""
Expand All @@ -240,3 +247,7 @@ def finalize_app(app: Flask) -> None:
# but imo this is better than entrypoints
for type in app.config["REQUESTS_REGISTERED_EVENT_TYPES"]:
current_event_type_registry.register_type(type)

ext.notification_recipients_resolvers_registry = app.config[
"NOTIFICATION_RECIPIENTS_RESOLVERS"
]
Empty file.
Empty file.
18 changes: 18 additions & 0 deletions oarepo_requests/notifications/builders/delete_published_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ..generators import EntityRecipient
from .oarepo import OARepoRequestActionNotificationBuilder


class DeletePublishedRecordRequestSubmitNotificationBuilder(
OARepoRequestActionNotificationBuilder
):
type = "delete-published-record-request-event.submit"

recipients = [EntityRecipient(key="request.receiver")] # email only


class DeletePublishedRecordRequestAcceptNotificationBuilder(
OARepoRequestActionNotificationBuilder
):
type = "delete-published-record-request-event.accept"

recipients = [EntityRecipient(key="request.created_by")]
38 changes: 38 additions & 0 deletions oarepo_requests/notifications/builders/oarepo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from __future__ import annotations

from typing import TYPE_CHECKING

from invenio_notifications.backends import EmailNotificationBackend
from invenio_notifications.models import Notification
from invenio_notifications.registry import EntityResolverRegistry
from invenio_notifications.services.builders import NotificationBuilder
from invenio_notifications.services.generators import EntityResolve, UserEmailBackend

if TYPE_CHECKING:
from invenio_requests.records.api import Request


class OARepoUserEmailBackend(UserEmailBackend):
backend_id = EmailNotificationBackend.id


class OARepoRequestActionNotificationBuilder(NotificationBuilder):

@classmethod
def build(cls, request: Request):
"""Build notification with context."""
return Notification(
type=cls.type,
context={
"request": EntityResolverRegistry.reference_entity(request),
"backend_ids": [
backend.backend_id for backend in cls.recipient_backends
],
},
)

context = [
EntityResolve(key="request"),
]

recipient_backends = [OARepoUserEmailBackend()]
18 changes: 18 additions & 0 deletions oarepo_requests/notifications/builders/publish.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ..generators import EntityRecipient
from .oarepo import OARepoRequestActionNotificationBuilder


class PublishDraftRequestSubmitNotificationBuilder(
OARepoRequestActionNotificationBuilder
):
type = "publish-draft-request-event.submit"

recipients = [EntityRecipient(key="request.receiver")] # email only


class PublishDraftRequestAcceptNotificationBuilder(
OARepoRequestActionNotificationBuilder
):
type = "publish-draft-request-event.accept"

recipients = [EntityRecipient(key="request.created_by")]
77 changes: 77 additions & 0 deletions oarepo_requests/notifications/generators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
from __future__ import annotations

from abc import abstractmethod
from typing import TYPE_CHECKING

from invenio_notifications.models import Recipient
from invenio_notifications.services.generators import RecipientGenerator
from invenio_records.dictutils import dict_lookup
from invenio_requests.proxies import current_requests

from oarepo_requests.proxies import current_notification_recipients_resolvers_registry

if TYPE_CHECKING:
from typing import Any

from invenio_notifications.models import Notification


class EntityRecipient(RecipientGenerator):
"""Recipient generator working as handler for generic entity."""

def __init__(self, key: str):
self.key = key

def __call__(self, notification: Notification, recipients: dict[str, Recipient]):
""""""
backend_ids = notification.context["backend_ids"]
entity_ref = dict_lookup(notification.context, self.key)
entity_type = list(entity_ref.keys())[0]
for backend_id in backend_ids:
generator = current_notification_recipients_resolvers_registry[entity_type][
backend_id
](entity_ref)
generator(notification, recipients)


class SpecificEntityRecipient(RecipientGenerator):
"""Superclass for implementations of recipient generators for specific entities."""

def __init__(self, key):
self.key = key # todo this is entity_reference, not path to entity as EntityRecipient, might be confusing

def __call__(self, notification: Notification, recipients: dict[str, Recipient]):
entity = self._resolve_entity()
recipients.update(self._get_recipients(entity))
return recipients

@abstractmethod
def _get_recipients(self, entity: Any) -> dict[str, Recipient]:
raise NotImplementedError()

def _resolve_entity(self) -> Any:
entity_type = list(self.key)[0]
registry = current_requests.entity_resolvers_registry

registered_resolvers = registry._registered_types
resolver = registered_resolvers.get(entity_type)
proxy = resolver.get_entity_proxy(self.key)
entity = proxy.resolve()
return entity


class UserEmailRecipient(SpecificEntityRecipient):
"""User email recipient generator for a notification."""

def _get_recipients(self, entity: Any) -> dict[str, Recipient]:
return {entity.email: Recipient(data={"email": entity.email})}


class GroupEmailRecipient(SpecificEntityRecipient):
"""Recipient generator returning emails of the members of the recipient group"""

def _get_recipients(self, entity: Any) -> dict[str, Recipient]:
return {
user.email: Recipient(data={"email": user.email})
for user in entity.users.all()
}
6 changes: 6 additions & 0 deletions oarepo_requests/proxies.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,9 @@
current_oarepo_requests_resource: OARepoRequestsResource = LocalProxy( # type: ignore
lambda: current_app.extensions["oarepo-requests"].requests_resource
)

current_notification_recipients_resolvers_registry = LocalProxy( # type: ignore
lambda: current_app.extensions[
"oarepo-requests"
].notification_recipients_resolvers_registry
)
6 changes: 4 additions & 2 deletions oarepo_requests/resolvers/interface.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from __future__ import annotations

from typing import Any, TYPE_CHECKING
import logging
from typing import TYPE_CHECKING, Any

from invenio_pidstore.errors import PersistentIdentifierError

from oarepo_requests.resolvers.ui import resolve
import logging

if TYPE_CHECKING:
from invenio_requests.records import Request
log = logging.getLogger(__name__)


# todo consider - we are not using this strictly in the ui context - so how should we separate these things in the future
def resolve_entity(entity: str, obj: Request, ctx: dict[str, Any]) -> dict:
"""Resolve the entity and put it into the context cache.
Expand Down
Loading

0 comments on commit 3e6853e

Please sign in to comment.