Skip to content

Commit

Permalink
Merge pull request #85 from oarepo/krist/be-492-expand-on-request-ins…
Browse files Browse the repository at this point in the history
…tance-seems-not-to-work-for-lter

Krist/be 492 expand on request instance seems not to work for lter
  • Loading branch information
mesemus authored Nov 22, 2024
2 parents 3a57df3 + b298246 commit 9938a67
Show file tree
Hide file tree
Showing 132 changed files with 4,025 additions and 1,263 deletions.
5 changes: 5 additions & 0 deletions .copyright.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Copyright (C) 2024 CESNET z.s.p.o.

oarepo-requests is free software; you can redistribute it and/or
modify it under the terms of the MIT License; see LICENSE file for more
details.
12 changes: 12 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[report]
exclude_lines =
pragma: no cover
^#
exclude_also =
if TYPE_CHECKING:
if __name__ == .__main__.:
if TYPE_CHECKING:
class .*\bProtocol\):
@(abc\.)?abstractmethod
raise AssertionError
raise NotImplementedError
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.10"
python-version: "3.12"
- name: Cache pip
uses: actions/cache@v4
with:
Expand Down
2 changes: 2 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[mypy]
disable_error_code = import-untyped, import-not-found
8 changes: 8 additions & 0 deletions oarepo_requests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Invenio extension for better handling of requests."""
8 changes: 8 additions & 0 deletions oarepo_requests/actions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Request actions."""
63 changes: 51 additions & 12 deletions oarepo_requests/actions/cascade_events.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,16 @@
#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Helper functions for cascading request update on topic change or delete."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any

from invenio_access.permissions import system_identity
from invenio_requests import (
current_events_service,
Expand All @@ -7,23 +20,35 @@
from invenio_requests.records import Request
from invenio_requests.resolvers.registry import ResolverRegistry

from oarepo_requests.utils import _reference_query_term
from oarepo_requests.utils import create_query_term_for_reference

if TYPE_CHECKING:
from invenio_records_resources.services.uow import UnitOfWork
from invenio_requests.customizations import EventType

from oarepo_requests.typing import EntityReference


def _str_from_ref(ref):
k, v = list(ref.items())[0]
def _str_from_ref(ref: EntityReference) -> str:
k, v = next(iter(ref.items()))
return f"{k}.{v}"


def _get_topic_ref_with_requests(topic):
topic_ref = ResolverRegistry.reference_entity(topic)
def _get_topic_reference(topic: Any) -> EntityReference:
return ResolverRegistry.reference_entity(topic)


def _get_requests_with_topic_reference(topic_ref: EntityReference) -> list[dict]:
requests_with_topic = current_requests_service.scan(
system_identity, extra_filter=_reference_query_term("topic", topic_ref)
system_identity,
extra_filter=create_query_term_for_reference("topic", topic_ref),
)
return requests_with_topic, topic_ref
return requests_with_topic


def _create_event(cur_request, payload, event_type, uow):
def _create_event(
cur_request: Request, payload: dict, event_type: type[EventType], uow: UnitOfWork
) -> None:
data = {"payload": payload}
current_events_service.create(
system_identity,
Expand All @@ -34,10 +59,20 @@ def _create_event(cur_request, payload, event_type, uow):
)


def update_topic(request, old_topic, new_topic, uow):
def update_topic(
request: Request, old_topic: Any, new_topic: Any, uow: UnitOfWork
) -> None:
"""Update topic on all requests with the old topic to the new topic.
:param request: Request on which the action is being executed, might be handled differently than the rest of the requests with the same topic
:param old_topic: Old topic
:param new_topic: New topic
:param uow: Unit of work
"""
from oarepo_requests.types.events import TopicUpdateEventType

requests_with_topic, old_topic_ref = _get_topic_ref_with_requests(old_topic)
old_topic_ref = _get_topic_reference(old_topic)
requests_with_topic = _get_requests_with_topic_reference(old_topic_ref)
new_topic_ref = ResolverRegistry.reference_entity(new_topic)
for request_from_search in requests_with_topic:
request_type = current_request_type_registry.lookup(
Expand All @@ -60,10 +95,14 @@ def update_topic(request, old_topic, new_topic, uow):
_create_event(cur_request, payload, TopicUpdateEventType, uow)


def cancel_requests_on_topic_delete(request, topic, uow):
def cancel_requests_on_topic_delete(
request: Request, topic: Any, uow: UnitOfWork
) -> None:
"""Cancel all requests with the topic that is being deleted."""
from oarepo_requests.types.events import TopicDeleteEventType

requests_with_topic, topic_ref = _get_topic_ref_with_requests(topic)
topic_ref = _get_topic_reference(topic)
requests_with_topic = _get_requests_with_topic_reference(topic_ref)
for request_from_search in requests_with_topic:
request_type = current_request_type_registry.lookup(
request_from_search["type"], quiet=True
Expand Down
125 changes: 105 additions & 20 deletions oarepo_requests/actions/components.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,87 @@
#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Request action components.
These components are called as context managers when an action is executed.
"""

from __future__ import annotations

import abc
import contextlib
from typing import Generator
from typing import (
TYPE_CHECKING,
Any,
ContextManager,
Generator,
override,
)

from invenio_requests.customizations import RequestActions
from invenio_requests.customizations import RequestAction, RequestActions, RequestType
from invenio_requests.errors import CannotExecuteActionError

from oarepo_requests.services.permissions.identity import request_active

if TYPE_CHECKING:
from flask_principal import Identity
from invenio_records_resources.services.uow import UnitOfWork
from invenio_requests.records.api import Request

from .generic import OARepoGenericActionMixin

type ActionType = (
OARepoGenericActionMixin | RequestAction
) # should be a type intersection, not yet in python


class RequestActionComponent(abc.ABC):
"""Abstract request action component."""

class RequestActionComponent:
@abc.abstractmethod
def apply(
self, identity, request_type, action, topic, uow, *args, **kwargs
) -> Generator:
"""
self,
identity: Identity,
request_type: RequestType,
action: ActionType,
topic: Any,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> ContextManager:
"""Apply the component.
:param action:
:param identity:
:param uow:
:param args:
:param kwargs:
:return:
Must return a context manager
:param identity: Identity of the user.
:param request_type: Request type.
:param action: Action being executed.
:param topic: Topic of the request.
:param uow: Unit of work.
:param args: Additional arguments.
:param kwargs: Additional keyword arguments.
"""


class RequestIdentityComponent(RequestActionComponent):
"""A component that adds a request active permission to the identity and removes it after processing."""

@override
@contextlib.contextmanager
def apply(self, identity, request_type, action, topic, uow, *args, **kwargs):
def apply(
self,
identity: Identity,
request_type: RequestType,
action: ActionType,
topic: Any,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> Generator[None, None, None]:
identity.provides.add(request_active)
try:
yield
Expand All @@ -36,8 +91,25 @@ def apply(self, identity, request_type, action, topic, uow, *args, **kwargs):


class WorkflowTransitionComponent(RequestActionComponent):
"""A component that applies a workflow transition after processing the action.
When the action is applied, the "status_to" of the request is looked up in
the workflow transitions for the request and if found, the topic's state is changed
to the target state.
"""

@override
@contextlib.contextmanager
def apply(self, identity, request_type, action, topic, uow, *args, **kwargs):
def apply(
self,
identity: Identity,
request_type: RequestType,
action: ActionType,
topic: Any,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> Generator[None, None, None]:
from oarepo_workflows.proxies import current_oarepo_workflows
from sqlalchemy.exc import NoResultFound

Expand All @@ -52,30 +124,43 @@ def apply(self, identity, request_type, action, topic, uow, *args, **kwargs):
NoResultFound
): # parent might be deleted - this is the case for delete_draft request type
return
target_state = transitions[action.status_to]
target_state = transitions[action.status_to] # type: ignore
if (
target_state and not topic.model.is_deleted
): # commit doesn't work on deleted record?
current_oarepo_workflows.set_state(
identity,
topic,
target_state,
request=action.request,
request=action.request, # type: ignore
uow=uow,
)


class AutoAcceptComponent(RequestActionComponent):
"""A component that auto-accepts the request if the receiver has auto-approve enabled."""

@override
@contextlib.contextmanager
def apply(self, identity, request_type, action, topic, uow, *args, **kwargs):
def apply(
self,
identity: Identity,
request_type: RequestType,
action: ActionType,
topic: Any,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> Generator[None, None, None]:
yield
if action.request.status != "submitted":
request: Request = action.request # type: ignore
if request.status != "submitted":
return
receiver_ref = action.request.receiver # this is <x>proxy, not dict
receiver_ref = request.receiver # this is <x>proxy, not dict
if not receiver_ref.reference_dict.get("auto_approve"):
return

action_obj = RequestActions.get_action(action.request, "accept")
action_obj = RequestActions.get_action(request, "accept")
if not action_obj.can_execute():
raise CannotExecuteActionError("accept")
action_obj.execute(identity, uow, *args, **kwargs)
34 changes: 32 additions & 2 deletions oarepo_requests/actions/delete_draft.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
#
# Copyright (C) 2024 CESNET z.s.p.o.
#
# oarepo-requests is free software; you can redistribute it and/or
# modify it under the terms of the MIT License; see LICENSE file for more
# details.
#
"""Actions for delete draft record request."""

from __future__ import annotations

from typing import TYPE_CHECKING, Any, override

from oarepo_runtime.datastreams.utils import get_record_service_for_record

from .cascade_events import cancel_requests_on_topic_delete
from .generic import OARepoAcceptAction

if TYPE_CHECKING:
from flask_principal import Identity
from invenio_drafts_resources.records import Record
from invenio_records_resources.services.uow import UnitOfWork
from invenio_requests.customizations import RequestType


class DeleteDraftAcceptAction(OARepoAcceptAction):
def apply(self, identity, request_type, topic, uow, *args, **kwargs):
"""Accept request for deletion of a draft record and delete the record."""

@override
def apply(
self,
identity: Identity,
request_type: RequestType,
topic: Record,
uow: UnitOfWork,
*args: Any,
**kwargs: Any,
) -> None:
topic_service = get_record_service_for_record(topic)
if not topic_service:
raise KeyError(f"topic {topic} service not found")
topic_service.delete_draft(identity, topic["id"], uow=uow, *args, **kwargs)
topic_service.delete_draft(identity, topic["id"], *args, uow=uow, **kwargs)
cancel_requests_on_topic_delete(self.request, topic, uow)
Loading

0 comments on commit 9938a67

Please sign in to comment.