Skip to content

Commit

Permalink
Covered tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mesemus committed Jul 9, 2024
1 parent b02e3e2 commit 1cb57ce
Show file tree
Hide file tree
Showing 14 changed files with 330 additions and 117 deletions.
17 changes: 17 additions & 0 deletions oarepo_workflows/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from .permissions import IfInState, WorkflowPermission, DefaultWorkflowPermissionPolicy, WorkflowPermissionPolicy
from .requests import WorkflowRequestPolicy, WorkflowRequest, WorkflowTransitions, RecipientGeneratorMixin, AutoRequest, AutoApprove
from .base import Workflow


__all__ = (
'IfInState',
'WorkflowPermission',
'DefaultWorkflowPermissionPolicy',
'WorkflowPermissionPolicy',
'WorkflowRequestPolicy',
'WorkflowRequest',
'WorkflowTransitions',
'RecipientGeneratorMixin',
'AutoRequest',
'AutoApprove',
)
21 changes: 21 additions & 0 deletions oarepo_workflows/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import dataclasses
from typing import Type
from flask_babel import LazyString

from .permissions import DefaultWorkflowPermissionPolicy
from .requests import WorkflowRequestPolicy


@dataclasses.dataclass
class Workflow:
label: str | LazyString
permissions_cls: Type[DefaultWorkflowPermissionPolicy]
requests_cls: Type[WorkflowRequestPolicy] = WorkflowRequestPolicy

@property
def permissions(self):
return self.permissions_cls

@property
def requests(self):
return self.requests_cls()
19 changes: 9 additions & 10 deletions oarepo_workflows/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,30 +44,29 @@ def default_workflow_getters(self):
def set_state(self, identity, record, value, *args, uow=None, **kwargs):
previous_value = record.state
record.state = value
for state_changed_notifier_ep in self.state_changed_notifiers:
state_changed_notifier = state_changed_notifier_ep
for state_changed_notifier in self.state_changed_notifiers:
state_changed_notifier(
identity, record, previous_value, value, *args, uow=uow, **kwargs
)

def get_default_workflow(self, default="default", *args, **kwargs):
for default_workflow_getter_ep in self.default_workflow_getters:
default_workflow_getter = default_workflow_getter_ep
default = default_workflow_getter(default, *args, **kwargs)
return default
def get_default_workflow(self, **kwargs):
for default_workflow_getter in self.default_workflow_getters:
default = default_workflow_getter(**kwargs)
if default:
return default
return "default"

def set_workflow(self, identity, record, value, *args, uow=None, **kwargs):
previous_value = record.parent["workflow"]
record.parent.workflow = value
for workflow_changed_notifier_ep in self.workflow_changed_notifiers:
workflow_changed_notifier = workflow_changed_notifier_ep
for workflow_changed_notifier in self.workflow_changed_notifiers:
workflow_changed_notifier(
identity, record, previous_value, value, *args, uow=uow, **kwargs
)

@property
def record_workflows(self):
return self.app.config["RECORD_WORKFLOWS"]
return self.app.config["WORKFLOWS"]

def init_app(self, app):
"""Flask application initialization."""
Expand Down
9 changes: 9 additions & 0 deletions oarepo_workflows/permissions/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .generators import IfInState, WorkflowPermission
from .policy import DefaultWorkflowPermissionPolicy, WorkflowPermissionPolicy

__all__ = (
'IfInState',
'WorkflowPermission',
'DefaultWorkflowPermissionPolicy',
'WorkflowPermissionPolicy',
)
103 changes: 32 additions & 71 deletions oarepo_workflows/permissions/generators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,91 +9,52 @@
from oarepo_workflows.proxies import current_oarepo_workflows


def needs_from_generators(generators, **kwargs):
if not generators:
return []
needs = [
g.needs(
**kwargs,
)
for g in generators
]
return set(chain.from_iterable(needs))


def reference_receiver_from_generators(generators, **kwargs):
if not generators:
return None
for generator in generators:
if hasattr(generator, 'reference_receiver'):
ref = generator.reference_receiver(**kwargs)
if ref:
return ref


def query_filters_from_generators(generators, **kwargs):
queries = [g.query_filter(**kwargs) for g in generators]
queries = [q for q in queries if q]
return reduce(operator.or_, queries) if queries else None


def _needs_from_workflow(workflow_id, action, record, **kwargs):
try:
# TODO: suspicious
generators = dict_lookup(
current_oarepo_workflows, f"{workflow_id}.permissions.{action}"
)
except KeyError:
return []
return needs_from_generators(generators, record, **kwargs)


def get_workflow_from_record(record, **kwargs):
if hasattr(record, "parent"):
record = record.parent
if hasattr(record, "workflow") and record.workflow:
return record.workflow
else:
# TODO: branch not in tests, should not return None
# when record has no workflow yet?
if "record" not in kwargs:
return current_oarepo_workflows.get_default_workflow(
record=record, **kwargs
)
else:
return current_oarepo_workflows.get_default_workflow(**kwargs)


def get_permission_class_from_workflow(record=None, action_name=None, **kwargs):
if record:
workflow_id = get_workflow_from_record(record)
else:
workflow_id = current_oarepo_workflows.get_default_workflow(**kwargs)

policy = dict_lookup(
current_oarepo_workflows.record_workflows, f"{workflow_id}.permissions"
)
return policy(action_name, **kwargs)


class WorkflowPermission(Generator):
def __init__(self, action):
super().__init__()
self._action = action

def _get_workflow_from_record(self, record, **kwargs):
if hasattr(record, "parent"):
record = record.parent
if hasattr(record, "workflow") and record.workflow:
return record.workflow
else:
return None

def _get_permission_class_from_workflow(self, record=None, action_name=None, **kwargs):
if record:
workflow_id = self._get_workflow_from_record(record)
else:
# TODO: should not we raise an exception here ???
workflow_id = current_oarepo_workflows.get_default_workflow(**kwargs)

policy = current_oarepo_workflows.record_workflows[workflow_id].permissions
return policy(action_name, **kwargs)

def _get_generators(self, record, **kwargs):
permission_class = get_permission_class_from_workflow(
permission_class = self._get_permission_class_from_workflow(
record, action_name=self._action, **kwargs
)
return getattr(permission_class, self._action, None)
return getattr(permission_class, self._action, None) or []

def needs(self, record=None, **kwargs):
generators = self._get_generators(record, **kwargs)
return needs_from_generators(generators, record=record, **kwargs)
needs = [
g.needs(
record=record,
**kwargs,
)
for g in generators
]
return set(chain.from_iterable(needs))

def query_filter(self, record=None, **kwargs):
generators = self._get_generators(record, **kwargs)
return query_filters_from_generators(generators, record=record, **kwargs)

queries = [g.query_filter(record=record, **kwargs) for g in generators]
queries = [q for q in queries if q]
return reduce(operator.or_, queries) if queries else None


class IfInState(ConditionalGenerator):
Expand Down
11 changes: 10 additions & 1 deletion oarepo_workflows/records/systemfields/workflow.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from invenio_db import db
from invenio_records.systemfields.model import ModelField
from oarepo_runtime.records.systemfields import MappingSystemFieldMixin

from ...proxies import current_oarepo_workflows


class WorkflowField(ModelField):
class WorkflowField(MappingSystemFieldMixin, ModelField):

def __init__(self):
self._workflow = None # added in db
Expand All @@ -16,3 +17,11 @@ def post_create(self, parent_record):

def pre_commit(self, parent_record):
super().pre_commit(parent_record)

@property
def mapping(self):
return {
self.attr_name: {
"type": "keyword"
}
}
10 changes: 10 additions & 0 deletions oarepo_workflows/requests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .policy import WorkflowRequestPolicy, WorkflowRequest, WorkflowTransitions, RecipientGeneratorMixin, AutoRequest, AutoApprove

__all__ = (
'WorkflowRequestPolicy',
'WorkflowRequest',
'WorkflowTransitions',
'RecipientGeneratorMixin',
'AutoRequest',
'AutoApprove',
)
12 changes: 0 additions & 12 deletions oarepo_workflows/requests/classes.py

This file was deleted.

107 changes: 107 additions & 0 deletions oarepo_workflows/requests/policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from typing import List, Tuple, Optional
import dataclasses
from invenio_records_permissions.generators import Generator
from invenio_access.permissions import SystemRoleNeed


@dataclasses.dataclass
class WorkflowRequest:
requesters: List[Generator] | Tuple[Generator]
recipients: List[Generator] | Tuple[Generator]
transitions: Optional['WorkflowTransitions'] = dataclasses.field(
default_factory=lambda: WorkflowTransitions())

def reference_receivers(self, **kwargs):
if not self.recipients:
return None
for generator in self.recipients:
if isinstance(generator, RecipientGeneratorMixin):
ref = generator.reference_receivers(**kwargs)
if ref:
return ref[0]
return None


@dataclasses.dataclass
class WorkflowTransitions:
"""
Transitions for a workflow request. If the request is submitted and submitted is filled,
the record (topic) of the request will be moved to state defined in submitted.
If the request is approved, the record will be moved to state defined in approved.
If the request is rejected, the record will be moved to state defined in rejected.
"""
submitted: Optional[str] = None
approved: Optional[str] = None
rejected: Optional[str] = None


class WorkflowRequestPolicy:
"""Base class for workflow request policies. Inherit from this class
and add properties to define specific requests for a workflow.
The name of the property is the request_type name and the value must be
an instance of WorkflowRequest.
Example:
class MyWorkflowRequests(WorkflowRequestPolicy):
delete_request = WorkflowRequest(
requesters = [
IfInState("published", RecordOwner())
],
recipients = [CommunityRole("curator")],
transitions: WorkflowTransitions(
submitted = 'considered_for_deletion',
approved = 'deleted',
rejected = 'published'
)
)
"""

def __getitem__(self, item):
try:
return getattr(self, item)
except AttributeError:
raise KeyError(f"Request type {item} not defined in {self.__class__.__name__}")


class RecipientGeneratorMixin:
"""
Mixin for permission generators that can be used as recipients in WorkflowRequest.
"""
def reference_receivers(self, record=None, request_type=None, **kwargs):
"""
Taken the context (will include record amd request type at least),
return the reference receiver(s) of the request.
Should return a list of receiver classes (whatever they are) or dictionary
serialization of the receiver classes.
Might return empty list or None to indicate that the generator does not
provide any receivers.
"""
raise NotImplementedError("Implement reference receiver in your code")


class AutoRequest(Generator):
"""
Auto request generator. This generator is used to automatically create a request
when a record is moved to a specific state.
"""
auto_request_need = SystemRoleNeed("auto_request")

def needs(self, **kwargs):
"""Enabling Needs."""
return [self.auto_request_need]


class AutoApprove(RecipientGeneratorMixin, Generator):
"""
Auto approve generator. If the generator is used within recipients of a request,
the request will be automatically approved when the request is submitted.
"""

def reference_receivers(self, record=None, request_type=None, **kwargs):
return [{
"auto_approve": True
}]
Loading

0 comments on commit 1cb57ce

Please sign in to comment.