Skip to content

Commit

Permalink
Merge pull request #69 from oarepo/krist/be-474-filters-on-apirequests
Browse files Browse the repository at this point in the history
Krist/be 474 filters on apirequests
  • Loading branch information
mesemus authored Oct 16, 2024
2 parents 72a1874 + f08a599 commit 1d571ed
Show file tree
Hide file tree
Showing 10 changed files with 287 additions and 70 deletions.
21 changes: 21 additions & 0 deletions oarepo_requests/ext.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
from functools import cached_property

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

from oarepo_requests.proxies import current_oarepo_requests
from oarepo_requests.resources.events.config import OARepoRequestsCommentsResourceConfig
from oarepo_requests.resources.events.resource import OARepoRequestsCommentsResource
from oarepo_requests.resources.oarepo.config import OARepoRequestsResourceConfig
Expand Down Expand Up @@ -49,6 +53,23 @@ def default_request_receiver(self, identity, request_type, record, creator, data
def allowed_receiver_ref_types(self):
return self.app.config.get("REQUESTS_ALLOWED_RECEIVERS", [])

@cached_property
def identity_to_entity_references_fncs(self):
group_name = "oarepo_requests.identity_to_entity_references"
return [
x.load() for x in importlib_metadata.entry_points().select(group=group_name)
]

def identity_to_entity_references(self, identity):
mappings = current_oarepo_requests.identity_to_entity_references_fncs
ret = [
mapping_fnc(identity) for mapping_fnc in mappings if mapping_fnc(identity)
]
flattened_ret = []
for mapping_result in ret:
flattened_ret += mapping_result
return flattened_ret

# copied from invenio_requests for now
def service_configs(self, app):
"""Customized service configs."""
Expand Down
7 changes: 7 additions & 0 deletions oarepo_requests/identity_to_entity_references.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
def user_mappings(identity):
return [{"user": identity.id}]


def group_mappings(identity):
roles = [n.value for n in identity.provides if n.method == "role"]
return [{"group": role_id} for role_id in roles]
43 changes: 26 additions & 17 deletions oarepo_requests/invenio_patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
RequestSearchOptions,
RequestsServiceConfig,
)
from invenio_requests.services.requests.params import IsOpenParam
from marshmallow import fields
from opensearch_dsl.query import Bool, Term
from opensearch_dsl.query import Bool

from oarepo_requests.proxies import current_oarepo_requests
from oarepo_requests.resources.ui import (
OARepoRequestEventsUIJSONSerializer,
OARepoRequestsUIJSONSerializer,
)
from oarepo_requests.services.oarepo.config import OARepoRequestsServiceConfig
from oarepo_requests.utils import _reference_query_term


class RequestOwnerFilterParam(FilterParam):
Expand All @@ -30,39 +33,45 @@ def apply(self, identity, search, params):
return search


from invenio_search.engine import dsl


class RequestReceiverFilterParam(FilterParam):
def apply(self, identity, search, params):
value = params.pop(self.param_name, None)
my_roles = [n.value for n in identity.provides if n.method == "role"]
terms = dsl.Q("match_none")
if value is not None:
search = search.filter(
Bool(
should=[
# explicitly myself
Term(**{f"{self.field_name}.user": identity.id}),
# my roles
*[
Term(**{f"{self.field_name}.group": role_id})
for role_id in my_roles
],
# TODO: add my communities where I have a role to accept requests
],
minimum_should_match=1,
)
)
references = current_oarepo_requests.identity_to_entity_references(identity)
for reference in references:
query_term = _reference_query_term(self.field_name, reference)
terms |= query_term
search = search.filter(Bool(filter=terms))
return search


class IsClosedParam(IsOpenParam):

def apply(self, identity, search, params):
"""Evaluate the is_closed parameter on the search."""
if params.get("is_closed") is True:
search = search.filter("term", **{self.field_name: True})
elif params.get("is_closed") is False:
search = search.filter("term", **{self.field_name: False})
return search


class EnhancedRequestSearchOptions(RequestSearchOptions):
params_interpreters_cls = RequestSearchOptions.params_interpreters_cls + [
RequestOwnerFilterParam.factory("mine", "created_by.user"),
RequestReceiverFilterParam.factory("assigned", "receiver"),
IsClosedParam.factory("is_closed"),
]


class ExtendedRequestSearchRequestArgsSchema(RequestSearchRequestArgsSchema):
mine = fields.Boolean()
assigned = fields.Boolean()
is_closed = fields.Boolean()


def override_invenio_requests_config(blueprint, *args, **kwargs):
Expand Down
6 changes: 6 additions & 0 deletions oarepo_requests/services/permissions/workflow_policies.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from invenio_records_permissions.generators import SystemProcess
from invenio_requests.customizations.event_types import CommentEventType, LogEventType
from invenio_requests.services.generators import Creator, Receiver
from invenio_requests.services.permissions import (
PermissionPolicy as InvenioRequestsPermissionPolicy,
)
from oarepo_workflows import DefaultWorkflowPermissions

from oarepo_requests.services.permissions.generators import (
EventCreatorsFromWorkflow,
IfEventType,
IfRequestType,
RequestActive,
RequestCreatorsFromWorkflow,
Expand Down Expand Up @@ -43,5 +46,8 @@ class CreatorsFromWorkflowRequestsPermissionPolicy(InvenioRequestsPermissionPoli

can_create_comment = [
SystemProcess(),
IfEventType(
[LogEventType.type_id, CommentEventType.type_id], [Creator(), Receiver()]
),
EventCreatorsFromWorkflow(),
]
12 changes: 9 additions & 3 deletions oarepo_requests/types/generic.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from invenio_records_resources.services.errors import PermissionDeniedError
from invenio_requests.customizations import RequestType
from invenio_requests.customizations.states import RequestState
from invenio_requests.proxies import current_requests_service

from oarepo_requests.errors import OpenRequestAlreadyExists
from oarepo_requests.utils import open_or_created_request_exists
from oarepo_requests.utils import open_request_exists

from ..actions.generic import (
OARepoAcceptAction,
Expand All @@ -19,6 +20,11 @@ class OARepoRequestType(RequestType):
dangerous = False
editable = True

@classmethod
@property
def available_statuses(cls):
return {**super().available_statuses, "created": RequestState.OPEN}

@classmethod
@property
def has_form(cls):
Expand Down Expand Up @@ -81,12 +87,12 @@ def stateful_description(self, identity, request):
# can be simulated by switching state to a one which does not allow create
class NonDuplicableOARepoRequestType(OARepoRequestType):
def can_create(self, identity, data, receiver, topic, creator, *args, **kwargs):
if open_or_created_request_exists(topic, self.type_id):
if open_request_exists(topic, self.type_id):
raise OpenRequestAlreadyExists(self, topic)
super().can_create(identity, data, receiver, topic, creator, *args, **kwargs)

@classmethod
def is_applicable_to(cls, identity, topic, *args, **kwargs):
if open_or_created_request_exists(topic, cls.type_id):
if open_request_exists(topic, cls.type_id):
return False
return super().is_applicable_to(identity, topic, *args, **kwargs)
11 changes: 2 additions & 9 deletions oarepo_requests/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,13 @@ def search_requests_filter(
return extra_filter


def open_or_created_request_exists(topic_or_reference, type_id):
def open_request_exists(topic_or_reference, type_id):
topic_reference = ResolverRegistry.reference_entity(topic_or_reference, raise_=True)
base_filter = search_requests_filter(
type_id=type_id, topic_reference=topic_reference, is_open=True
)
created_filter = search_requests_filter(
type_id=type_id,
topic_reference=topic_reference,
is_open=False,
add_filter=dsl.Q("term", **{"status": "created"}),
)
results = current_requests_service.search(
system_identity, extra_filter=base_filter | created_filter
system_identity, extra_filter=base_filter
).hits
return bool(list(results))

Expand Down Expand Up @@ -161,7 +155,6 @@ def get_receiver_for_request_type(request_type, identity, topic):
except KeyError:
return None


receivers = workflow_request.reference_receivers(
identity=identity, record=topic, request_type=request_type, creator=identity
)
Expand Down
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,7 @@ invenio_base.finalize_app =
oarepo_requests = oarepo_requests.ext:finalize_app
invenio_base.api_finalize_app =
oarepo_requests = oarepo_requests.ext:api_finalize_app

oarepo_requests.identity_to_entity_references =
user = oarepo_requests.identity_to_entity_references:user_mappings
group = oarepo_requests.identity_to_entity_references:group_mappings
66 changes: 50 additions & 16 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,19 @@
)
from oarepo_requests.types import ModelRefTypes, NonDuplicableOARepoRequestType
from oarepo_requests.types.events.topic_update import TopicUpdateEventType
from tests.test_requests.utils import link_api2testclient

can_comment_only_receiver = [
Receiver(),
SystemProcess(),
]


class TestEventType(CommentEventType):
type_id = "test"
"""""" # to test permissions


events_only_receiver_can_comment = {
CommentEventType.type_id: WorkflowEvent(submitters=can_comment_only_receiver),
LogEventType.type_id: WorkflowEvent(
Expand All @@ -71,6 +78,7 @@
TopicUpdateEventType.type_id: WorkflowEvent(
submitters=InvenioRequestsPermissionPolicy.can_create_comment
),
TestEventType.type_id: WorkflowEvent(submitters=can_comment_only_receiver),
}


Expand Down Expand Up @@ -117,6 +125,13 @@ class DefaultRequests(WorkflowRequestPolicy):
)


class RequestsWithDifferentRecipients(DefaultRequests):
another_topic_updating = WorkflowRequest(
requesters=[AnyUser()],
recipients=[UserGenerator(1)],
)


class RequestsWithApprove(WorkflowRequestPolicy):
publish_draft = WorkflowRequest(
requesters=[IfInState("approved", [AutoRequest()])],
Expand Down Expand Up @@ -255,6 +270,13 @@ class WithApprovalPermissions(RequestBasedWorkflowPermissions):
permission_policy_cls=WithApprovalPermissions,
request_policy_cls=RequestsWithAnotherTopicUpdatingRequestType,
),
"different_recipients": Workflow(
label=_(
"Workflow with draft requests with different recipients to test param interpreters"
),
permission_policy_cls=TestWorkflowPermissions,
request_policy_cls=RequestsWithDifferentRecipients,
),
}


Expand Down Expand Up @@ -443,7 +465,7 @@ def _result(topic_id, request_id):

@pytest.fixture(scope="module")
def app_config(app_config):
app_config["REQUESTS_REGISTERED_EVENT_TYPES"] = [LogEventType(), CommentEventType()]
app_config["REQUESTS_REGISTERED_EVENT_TYPES"] = [TestEventType(), LogEventType(), CommentEventType()]
app_config["SEARCH_HOSTS"] = [
{
"host": os.environ.get("OPENSEARCH_HOST", "localhost"),
Expand Down Expand Up @@ -484,21 +506,6 @@ def request_events_service(app):
return service


@pytest.fixture()
def create_request(requests_service):
"""Request Factory fixture."""

def _create_request(identity, input_data, receiver, request_type, **kwargs):
"""Create a request."""
# Need to use the service to get the id
item = requests_service.create(
identity, input_data, request_type=request_type, receiver=receiver, **kwargs
)
return item._request

return _create_request


@pytest.fixture()
def users(app, db, UserFixture):
user1 = UserFixture(
Expand Down Expand Up @@ -704,3 +711,30 @@ def _create_request_from_link(request_types_json, request_type):
return selected_entry["links"]["actions"]["create"]

return _create_request_from_link


@pytest.fixture
def create_request_by_link(get_request_link):
def _create_request(client, record, request_type):
applicable_requests = client.get(
link_api2testclient(record.json["links"]["applicable-requests"])
).json["hits"]["hits"]
create_link = link_api2testclient(
get_request_link(applicable_requests, request_type)
)
create_response = client.post(create_link)
return create_response

return _create_request


@pytest.fixture
def submit_request_by_link(create_request_by_link):
def _submit_request(client, record, request_type):
create_response = create_request_by_link(client, record, request_type)
submit_response = client.post(
link_api2testclient(create_response.json["links"]["actions"]["submit"])
)
return submit_response

return _submit_request
Loading

0 comments on commit 1d571ed

Please sign in to comment.