From bf5f7d73e76713321824abfb5636f2e3c41d364c Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 14:22:09 +0100
Subject: [PATCH 01/21] Moved AutoApprove to workflows

---
 oarepo_workflows/errors.py                    |  14 +-
 oarepo_workflows/ext.py                       |  19 ++
 oarepo_workflows/requests/__init__.py         |   8 +
 oarepo_workflows/requests/policy.py           | 224 ++++++++++++++----
 oarepo_workflows/resolvers/__init__.py        |   0
 .../resolvers/auto_approve/__init__.py        |  42 ++++
 .../services/auto_approve/__init__.py         |  11 +
 .../services/components/workflow.py           |   1 -
 oarepo_workflows/views/__init__.py            |   0
 oarepo_workflows/views/api.py                 |  14 ++
 oarepo_workflows/views/app.py                 |  14 ++
 setup.cfg                                     |  10 +
 tests/test_workflow_requests.py               |  51 +++-
 13 files changed, 356 insertions(+), 52 deletions(-)
 create mode 100644 oarepo_workflows/resolvers/__init__.py
 create mode 100644 oarepo_workflows/resolvers/auto_approve/__init__.py
 create mode 100644 oarepo_workflows/services/auto_approve/__init__.py
 create mode 100644 oarepo_workflows/views/__init__.py
 create mode 100644 oarepo_workflows/views/api.py
 create mode 100644 oarepo_workflows/views/app.py

diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index dc8d46a..9a26ab6 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -2,8 +2,18 @@
 
 
 class MissingWorkflowError(ValidationError):
-    """"""
+    """
+    Exception raised when a required workflow is missing.
+    Attributes:
+        message -- explanation of the error
+    """
+    
 
 
 class InvalidWorkflowError(ValidationError):
-    """"""
+    """
+    Exception raised when a workflow is invalid.
+    Attributes:
+        message -- explanation of the error
+    """
+
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 36ed96d..dcbe4b3 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -7,6 +7,8 @@
 
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
+from oarepo_workflows.services.auto_approve import AutoApproveEntityService, \
+    AutoApproveEntityServiceConfig
 
 
 class OARepoWorkflows(object):
@@ -15,6 +17,7 @@ def __init__(self, app=None):
         if app:
             self.init_config(app)
             self.init_app(app)
+            self.init_services(app)
 
     def init_config(self, app):
         """Initialize configuration."""
@@ -31,6 +34,11 @@ def init_config(self, app):
 
         app.config.setdefault("WORKFLOWS", ext_config.WORKFLOWS)
 
+    def init_services(self, app):
+        self.autoapprove_service = AutoApproveEntityService(
+            config=AutoApproveEntityServiceConfig()
+        )
+
     @cached_property
     def state_changed_notifiers(self):
         group_name = "oarepo_workflows.state_changed_notifiers"
@@ -124,3 +132,14 @@ def init_app(self, app):
         """Flask application initialization."""
         self.app = app
         app.extensions["oarepo-workflows"] = self
+
+def finalize_app(app):
+    records_resources = app.extensions["invenio-records-resources"]
+
+    ext = app.extensions["oarepo-workflows"]
+
+    records_resources.registry.register(
+        ext.autoapprove_service,
+        service_id=ext.autoapprove_service.config.service_id,
+    )
+
diff --git a/oarepo_workflows/requests/__init__.py b/oarepo_workflows/requests/__init__.py
index 3cb96d2..3f48891 100644
--- a/oarepo_workflows/requests/__init__.py
+++ b/oarepo_workflows/requests/__init__.py
@@ -4,6 +4,10 @@
     WorkflowRequestEscalation,
     WorkflowRequestPolicy,
     WorkflowTransitions,
+    AutoApprove,
+    AutoRequest,
+    RequesterGenerator,
+    RecipientEntityReference
 )
 
 __all__ = (
@@ -12,4 +16,8 @@
     "WorkflowTransitions",
     "RecipientGeneratorMixin",
     "WorkflowRequestEscalation",
+    "AutoApprove",
+    "AutoRequest",
+    "RequesterGenerator",
+    "RecipientEntityReference"
 )
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index c2495c4..a9cc68b 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -1,56 +1,85 @@
+from __future__ import annotations
+
 import dataclasses
-import inspect
+from functools import cached_property
 from datetime import timedelta
+from logging import getLogger
 from typing import Dict, List, Optional, Tuple
 
+from flask_principal import Need, Permission, Identity
 from invenio_access.permissions import SystemRoleNeed
 from invenio_records_permissions.generators import Generator
 
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.requests.events import WorkflowEvent
 
+log = getLogger(__name__)
+
 
 @dataclasses.dataclass
 class WorkflowRequest:
+    """
+    Workflow request definition. The request is defined by the requesters and recipients.
+    The requesters are the generators that define who can submit the request. The recipients
+    are the generators that define who can approve the request.
+    """
+
     requesters: List[Generator] | Tuple[Generator]
+    """Generators that define who can submit the request."""
+
     recipients: List[Generator] | Tuple[Generator]
+    """Generators that define who can approve the request."""
+
     events: Dict[str, WorkflowEvent] = dataclasses.field(default_factory=lambda: {})
-    transitions: Optional["WorkflowTransitions"] = dataclasses.field(
+    """Events that can be submitted with the request."""
+
+    transitions: Optional[WorkflowTransitions] = dataclasses.field(
         default_factory=lambda: WorkflowTransitions()
     )
-    escalations: Optional[List["WorkflowRequestEscalation"]] = None
-
-    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
+    """Transitions applied to the state of the topic of the request."""
 
-    def needs(self, **kwargs):
-        return {
-            need for generator in self.requesters for need in generator.needs(**kwargs)
-        }
+    escalations: Optional[List[WorkflowRequestEscalation]] = None
+    """Escalations applied to the request if not approved/declined in time."""
 
-    def excludes(self, **kwargs):
-        return {
-            exclude
-            for generator in self.requesters
-            for exclude in generator.excludes(**kwargs)
-        }
+    @cached_property
+    def requester_generator(self) -> Generator:
+        """
+        Return the requesters as a single requester generator.
+        """
+        return RequesterGenerator(self.requesters)
 
-    def query_filters(self, **kwargs):
-        return [
-            query_filter
-            for generator in self.requesters
-            for query_filter in generator.query_filter(**kwargs)
-        ]
+    def recipient_entity_reference(self, **context) -> Dict | None:
+        """
+        Return the reference receiver of the workflow request with the given context.
+        Note: invenio supports only one receiver, so the first one is returned at the moment.
+        Later on, a composite receiver can be implemented.
+
+        :param context: Context of the request.
+        """
+        return RecipientEntityReference(self, **context)
+
+    def is_applicable(self, identity: Identity, **context) -> bool:
+        """
+        Check if the request is applicable for the identity and context (which might include record, community, ...).
+
+        :param identity: Identity of the requester.
+        :param context: Context of the request that is passed to the requester generators.
+        """
+        try:
+            p = Permission(*self.requester_generator.needs(**context))
+            if not p.needs:
+                return False
+            p.excludes.update(self.requester_generator.excludes(**context))
+            return p.allows(identity)
+        except Exception as e:
+            log.exception("Error checking request applicability: %s", e)
+            return False
 
     @property
-    def allowed_events(self):
+    def allowed_events(self) -> Dict[str, WorkflowEvent]:
+        """
+        Return the allowed events for the workflow request.
+        """
         return current_oarepo_workflows.default_workflow_event_submitters | self.events
 
 
@@ -119,27 +148,30 @@ def __getitem__(self, item):
                 f"Request type {item} not defined in {self.__class__.__name__}"
             )
 
+    @cached_property
     def items(self):
-        return inspect.getmembers(self, lambda x: isinstance(x, WorkflowRequest))
-
-
-class RecipientGeneratorMixin:
-    """
-    Mixin for permission generators that can be used as recipients in WorkflowRequest.
-    """
-
-    def reference_receivers(self, record=None, request_type=None, **kwargs):
+        ret = []
+        parent_attrs = set(dir(WorkflowRequestPolicy))
+        for attr in dir(self.__class__):
+            if parent_attrs and attr in parent_attrs:
+                continue
+            if attr.startswith("_"):
+                continue
+            possible_request = getattr(self, attr, None)
+            if isinstance(possible_request, WorkflowRequest):
+                ret.append((attr, possible_request))
+        return ret
+
+    def applicable_workflow_requests(self, identity, **context):
         """
-        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.
+        Return a list of applicable requests for the identity and context.
         """
-        raise NotImplementedError("Implement reference receiver in your code")
+        ret = []
+
+        for name, request in self.items:
+            if request.is_applicable(identity, **context):
+                ret.append((name, request))
+        return ret
 
 
 auto_request_need = SystemRoleNeed("auto_request")
@@ -157,6 +189,24 @@ def needs(self, **kwargs):
         return [auto_request_need]
 
 
+class RecipientGeneratorMixin:
+    """
+    Mixin for permission generators that can be used as recipients in WorkflowRequest.
+    """
+
+    def reference_receivers(self, record=None, request_type=None, **kwargs) -> List[Dict]:
+        """
+        Taken the context (will include record amd request type at least),
+        return the reference receiver(s) of the request.
+
+        Should return a list of 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 AutoApprove(RecipientGeneratorMixin, Generator):
     """
     Auto approve generator. If the generator is used within recipients of a request,
@@ -165,3 +215,81 @@ class AutoApprove(RecipientGeneratorMixin, Generator):
 
     def reference_receivers(self, record=None, request_type=None, **kwargs):
         return [{"auto_approve": "true"}]
+
+
+class RequesterGenerator(Generator):
+    def __init__(self, requesters) -> None:
+        super().__init__()
+        self.requesters = requesters
+
+    def needs(self, **context) -> set[Need]:
+        """
+        Generate a set of needs that requester needs to have in order to create the request.
+
+        :param context: Context of the request.
+        :return: Set of needs.
+        """
+
+        return {
+            need for generator in self.requesters for need in generator.needs(**context)
+        }
+
+    def excludes(self, **context) -> set[Need]:
+        """
+        Generate a set of needs that requester must not have in order to create the request.
+
+        :param context: Context of the request.
+        :return: Set of needs.
+        """
+        return {
+            exclude
+            for generator in self.requesters
+            for exclude in generator.excludes(**context)
+        }
+
+    def query_filters(self, **context) -> list[dict]:
+        """
+        Generate a list of opensearch query filters to get the listing of matching requests.
+
+        :param context: Context of the request.
+        """
+
+        return [
+            query_filter
+            for generator in self.requesters
+            for query_filter in generator.query_filter(**context)
+        ]
+
+
+def RecipientEntityReference(
+    request: WorkflowRequest, **context
+) -> Dict | None:
+    """
+    Return the reference receiver of the workflow request with the given context.
+    Note: invenio supports only one receiver, so the first one is returned at the moment.
+    Later on, a composite receiver can be implemented.
+
+    :param request: Workflow request.
+    :param context: Context of the request.
+
+    :return: Reference receiver as a dictionary or None if no receiver has been resolved.
+
+    Implementation note: intentionally looks like a class, later on might be converted into one extending from dict.
+    """
+
+    if not request.recipients:
+        return None
+
+    all_receivers = []
+    for generator in request.recipients:
+        if isinstance(generator, RecipientGeneratorMixin):
+            ref: list[dict] = generator.reference_receivers(**context)
+            if ref:
+                all_receivers.extend(ref)
+
+    if all_receivers:
+        if len(all_receivers) > 1:
+            log.debug("Multiple receivers for request %s: %s", request, all_receivers)
+        return all_receivers[0]
+
+    return None
diff --git a/oarepo_workflows/resolvers/__init__.py b/oarepo_workflows/resolvers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
new file mode 100644
index 0000000..2df88e8
--- /dev/null
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -0,0 +1,42 @@
+from invenio_records_resources.references.entity_resolvers import EntityProxy
+from invenio_records_resources.references.entity_resolvers.base import EntityResolver
+from flask_principal import Need
+
+
+class AutoApprover:
+    def __init__(self, value: bool):
+        self.value = value
+
+
+class AutoApproveProxy(EntityProxy):
+    def _resolve(self) -> AutoApprover:
+        value = self._parse_ref_dict_id()
+        return AutoApprover(value)
+
+    def get_needs(self, ctx=None) -> list[Need]:
+        return []  # granttokens calls this
+
+    def pick_resolved_fields(self, identity, resolved_dict) -> dict:
+        return {"auto_approve": resolved_dict["id"]}
+
+
+class AutoApproveResolver(EntityResolver):
+    type_id = "auto_approve"
+
+    def __init__(self):
+        self.type_key = self.type_id
+        super().__init__(
+            "auto_approve",
+        )
+
+    def matches_reference_dict(self, ref_dict):
+        return self._parse_ref_dict_type(ref_dict) == self.type_id
+
+    def _reference_entity(self, entity):
+        return {self.type_key: str(entity.value)}
+
+    def matches_entity(self, entity):
+        return isinstance(entity, AutoApprover)
+
+    def _get_entity_proxy(self, ref_dict):
+        return AutoApproveProxy(self, ref_dict)
diff --git a/oarepo_workflows/services/auto_approve/__init__.py b/oarepo_workflows/services/auto_approve/__init__.py
new file mode 100644
index 0000000..43a43ad
--- /dev/null
+++ b/oarepo_workflows/services/auto_approve/__init__.py
@@ -0,0 +1,11 @@
+from oarepo_runtime.services.entity.config import KeywordEntityServiceConfig
+from oarepo_runtime.services.entity.service import KeywordEntityService
+
+
+class AutoApproveEntityServiceConfig(KeywordEntityServiceConfig):
+    service_id = "auto_approve"
+    keyword = "auto_approve"
+
+
+class AutoApproveEntityService(KeywordEntityService):
+    pass
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 86c7213..70d7fb1 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -5,7 +5,6 @@
 
 
 class WorkflowComponent(ServiceComponent):
-
     def create(self, identity, data=None, record=None, **kwargs):
         try:
             workflow_id = data["parent"]["workflow"]
diff --git a/oarepo_workflows/views/__init__.py b/oarepo_workflows/views/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
new file mode 100644
index 0000000..21aab4b
--- /dev/null
+++ b/oarepo_workflows/views/api.py
@@ -0,0 +1,14 @@
+from flask import Blueprint
+
+def create_api_blueprint(app):
+    """Create requests blueprint."""
+    blueprint = Blueprint("oarepo-workflows", __name__)
+
+    def register_autoapprove_entity_resolver(state):
+        from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
+        requests = app.extensions["invenio-requests"]
+        requests.entity_resolvers_registry.register_type(AutoApproveResolver())
+
+    blueprint.record_once(register_autoapprove_entity_resolver)
+
+    return blueprint
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
new file mode 100644
index 0000000..9ab1b08
--- /dev/null
+++ b/oarepo_workflows/views/app.py
@@ -0,0 +1,14 @@
+from flask import Blueprint
+
+def create_app_blueprint(app):
+    """Create requests blueprint."""
+    blueprint = Blueprint("oarepo-workflows", __name__)
+
+    def register_autoapprove_entity_resolver(state):
+        from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
+        requests = app.extensions["invenio-requests"]
+        requests.entity_resolvers_registry.register_type(AutoApproveResolver())
+
+    blueprint.record_once(register_autoapprove_entity_resolver)
+
+    return blueprint
diff --git a/setup.cfg b/setup.cfg
index 35b8f9c..84900c5 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -31,3 +31,13 @@ invenio_base.apps =
     oarepo_workflows = oarepo_workflows.ext:OARepoWorkflows
 invenio_base.api_apps =
     oarepo_workflows = oarepo_workflows.ext:OARepoWorkflows
+invenio_requests.entity_resolvers =
+    auto_approve = oarepo_workflows.resolvers.auto_approve:AutoApproveResolver
+invenio_base.finalize_app =
+    oarepo_workflows = oarepo_workflows.ext:finalize_app
+invenio_base.api_finalize_app =
+    oarepo_workflows = oarepo_workflows.ext:finalize_app
+invenio_base.api_blueprints =
+    oarepo_workflows = oarepo_workflows.views.api:create_api_blueprint
+invenio_base.blueprints =
+    oarepo_workflows = oarepo_workflows.views.app:create_app_blueprint
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index 71417ea..b12e975 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -1,9 +1,15 @@
+from types import SimpleNamespace
+
 from invenio_records_permissions.generators import Generator
 from oarepo_runtime.services.generators import RecordOwners
 
+from oarepo_workflows import WorkflowRequestPolicy
 from oarepo_workflows.requests import RecipientGeneratorMixin, WorkflowRequest
 from thesis.thesis.records.api import ThesisRecord
 
+from flask_principal import Identity, UserNeed, RoleNeed
+from oarepo_runtime.services.permissions.generators import UserWithRole
+
 
 class TestRecipient(RecipientGeneratorMixin, Generator):
     def reference_receivers(self, record=None, request_type=None, **kwargs):
@@ -22,10 +28,53 @@ def test_workflow_requests(users, logged_client, search_clear, record_service):
         recipients=[NullRecipient(), TestRecipient()],
     )
     rec = ThesisRecord.create({})
-    assert req.reference_receivers(record=rec) == {"user": "1"}
+    assert req.recipient_entity_reference(record=rec) == {"user": "1"}
 
 
 def test_request_policy_access(app):
     request_policy = app.config["WORKFLOWS"]["my_workflow"].requests()
     assert getattr(request_policy, "delete_request", None)
     assert not getattr(request_policy, "non_existing_request", None)
+
+def test_is_applicable(users, logged_client, search_clear, record_service):
+    req = WorkflowRequest(
+        requesters=[RecordOwners()],
+        recipients=[NullRecipient(), TestRecipient()],
+    )
+    id1=Identity(id=1)
+    id1.provides.add(UserNeed(1))
+
+    id2=Identity(id=2)
+    id2.provides.add(UserNeed(2))
+
+    record = SimpleNamespace(parent=SimpleNamespace(owners = [SimpleNamespace(id=1)]))
+    assert req.is_applicable(id2, record=record) is False
+    assert req.is_applicable(id1, record=record) is True
+
+def test_list_applicable_requests(users, logged_client, search_clear, record_service):
+    class R(WorkflowRequestPolicy):
+        req = WorkflowRequest(
+            requesters=[RecordOwners()],
+            recipients=[NullRecipient(), TestRecipient()],
+        )
+        req1 = WorkflowRequest(
+            requesters=[
+                UserWithRole("administrator"),
+            ],
+            recipients=[NullRecipient(), TestRecipient()],
+        )
+
+    requests = R()
+
+    id1=Identity(id=1)
+    id1.provides.add(UserNeed(1))
+
+    id2=Identity(id=2)
+    id2.provides.add(UserNeed(2))
+    id2.provides.add(RoleNeed("administrator"))
+
+    record = SimpleNamespace(parent=SimpleNamespace(owners = [SimpleNamespace(id=1)]))
+
+    assert set(x[0] for x in requests.applicable_workflow_requests(id1, record=record)) == {'req'}
+
+    assert set(x[0] for x in requests.applicable_workflow_requests(id2, record=record)) == {'req1'}

From abb70559f2ae27a8fe8bf2a220bf75298f6bffd0 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 15:20:33 +0100
Subject: [PATCH 02/21] Typing 1st round

---
 oarepo_workflows/base.py                            |  2 +-
 oarepo_workflows/ext.py                             | 12 ++++++------
 oarepo_workflows/records/systemfields/state.py      |  8 ++++----
 oarepo_workflows/records/systemfields/workflow.py   |  2 +-
 oarepo_workflows/resolvers/auto_approve/__init__.py |  4 ++--
 oarepo_workflows/services/permissions/generators.py |  6 +++---
 oarepo_workflows/services/permissions/policy.py     |  2 +-
 oarepo_workflows/views/api.py                       |  2 +-
 oarepo_workflows/views/app.py                       |  2 +-
 9 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/oarepo_workflows/base.py b/oarepo_workflows/base.py
index ad38159..0026f39 100644
--- a/oarepo_workflows/base.py
+++ b/oarepo_workflows/base.py
@@ -22,7 +22,7 @@ def requests(self):
         """Return instance of request policy for this workflow."""
         return self.request_policy_cls()
 
-    def __post_init__(self):
+    def __post_init__(self) -> None:
         assert not issubclass(self.permission_policy_cls, WorkflowPermissionPolicy)
         assert issubclass(self.permission_policy_cls, DefaultWorkflowPermissions)
 
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index dcbe4b3..1ce5a8a 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -13,13 +13,13 @@
 
 class OARepoWorkflows(object):
 
-    def __init__(self, app=None):
+    def __init__(self, app=None) -> None:
         if app:
             self.init_config(app)
             self.init_app(app)
             self.init_services(app)
 
-    def init_config(self, app):
+    def init_config(self, app) -> None:
         """Initialize configuration."""
         from . import ext_config
 
@@ -34,7 +34,7 @@ def init_config(self, app):
 
         app.config.setdefault("WORKFLOWS", ext_config.WORKFLOWS)
 
-    def init_services(self, app):
+    def init_services(self, app) -> None:
         self.autoapprove_service = AutoApproveEntityService(
             config=AutoApproveEntityServiceConfig()
         )
@@ -55,7 +55,7 @@ def workflow_changed_notifiers(self):
 
     def set_state(
         self, identity, record, value, *args, uow=None, commit=True, **kwargs
-    ):
+    ) -> None:
         previous_value = record.state
         record.state = value
         if commit:
@@ -128,12 +128,12 @@ def get_workflow(self, record):
                 f"Workflow {record.parent.workflow} on record {self._get_id_from_record(record)} doesn't exist."
             )
 
-    def init_app(self, app):
+    def init_app(self, app) -> None:
         """Flask application initialization."""
         self.app = app
         app.extensions["oarepo-workflows"] = self
 
-def finalize_app(app):
+def finalize_app(app) -> None:
     records_resources = app.extensions["invenio-records-resources"]
 
     ext = app.extensions["oarepo-workflows"]
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index 13499aa..b4fe485 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -3,15 +3,15 @@
 
 
 class RecordStateField(MappingSystemFieldMixin, SystemField):
-    def __init__(self, key="state", initial="draft", config=None):
+    def __init__(self, key="state", initial="draft", config=None) -> None:
         self._config = config
         self._initial = initial
         super().__init__(key=key)
 
-    def post_create(self, record):
+    def post_create(self, record) -> None:
         self.set_dictkey(record, self._initial)
 
-    def post_init(self, record, data, model=None, **kwargs):
+    def post_init(self, record, data, model=None, **kwargs) -> None:
         if not record.state:
             self.set_dictkey(record, self._initial)
 
@@ -21,7 +21,7 @@ def __get__(self, record, owner=None):
             return self
         return self.get_dictkey(record)
 
-    def __set__(self, record, value):
+    def __set__(self, record, value) -> None:
         self.set_dictkey(record, value)
 
     @property
diff --git a/oarepo_workflows/records/systemfields/workflow.py b/oarepo_workflows/records/systemfields/workflow.py
index 4750d2d..0a9b761 100644
--- a/oarepo_workflows/records/systemfields/workflow.py
+++ b/oarepo_workflows/records/systemfields/workflow.py
@@ -4,7 +4,7 @@
 
 class WorkflowField(MappingSystemFieldMixin, ModelField):
 
-    def __init__(self):
+    def __init__(self) -> None:
         self._workflow = None  # added in db
         super().__init__(model_field_name="workflow", key="workflow")
 
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index 2df88e8..45fd471 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -4,7 +4,7 @@
 
 
 class AutoApprover:
-    def __init__(self, value: bool):
+    def __init__(self, value: bool) -> None:
         self.value = value
 
 
@@ -23,7 +23,7 @@ def pick_resolved_fields(self, identity, resolved_dict) -> dict:
 class AutoApproveResolver(EntityResolver):
     type_id = "auto_approve"
 
-    def __init__(self):
+    def __init__(self) -> None:
         self.type_key = self.type_id
         super().__init__(
             "auto_approve",
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 12366eb..c0fce77 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -19,7 +19,7 @@ def _make_query(generators, **kwargs):
 
 
 class WorkflowPermission(Generator):
-    def __init__(self, action=None):
+    def __init__(self, action=None) -> None:
         # might not be needed in subclasses
         super().__init__()
         self._action = action
@@ -54,7 +54,7 @@ def query_filter(self, record=None, **kwargs):
 
 
 class IfInState(ConditionalGenerator):
-    def __init__(self, state, then_):
+    def __init__(self, state, then_) -> None:
         super().__init__(then_, else_=[])
         self.state = state
 
@@ -76,7 +76,7 @@ def query_filter(self, **kwargs):
 
 
 class SameAs(Generator):
-    def __init__(self, as_):
+    def __init__(self, as_) -> None:
         self.as_ = as_
 
     def _generators(self, policy, **kwargs):
diff --git a/oarepo_workflows/services/permissions/policy.py b/oarepo_workflows/services/permissions/policy.py
index 500cb98..8e51f77 100644
--- a/oarepo_workflows/services/permissions/policy.py
+++ b/oarepo_workflows/services/permissions/policy.py
@@ -37,7 +37,7 @@ class MyWorkflowPermissions(DefaultWorkflowPermissions):
 
     system_process = SystemProcess()
 
-    def __init__(self, action_name=None, **over):
+    def __init__(self, action_name=None, **over) -> None:
         can = getattr(self, f"can_{action_name}")
         if self.system_process not in can:
             can.append(self.system_process)
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index 21aab4b..08a7625 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -4,7 +4,7 @@ def create_api_blueprint(app):
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state):
+    def register_autoapprove_entity_resolver(state) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index 9ab1b08..9f0f387 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -4,7 +4,7 @@ def create_app_blueprint(app):
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state):
+    def register_autoapprove_entity_resolver(state) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())

From f60cca71c515495358202573ddb4c2e169c9d0e2 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 15:21:38 +0100
Subject: [PATCH 03/21] Typing 2nd round

---
 oarepo_workflows/ext.py                             | 4 ++--
 oarepo_workflows/records/systemfields/state.py      | 2 +-
 oarepo_workflows/services/permissions/generators.py | 3 ++-
 oarepo_workflows/services/permissions/policy.py     | 3 ++-
 4 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 1ce5a8a..310ef4d 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -54,7 +54,7 @@ def workflow_changed_notifiers(self):
         ]
 
     def set_state(
-        self, identity, record, value, *args, uow=None, commit=True, **kwargs
+        self, identity, record, value, *args, uow=None, commit: bool=True, **kwargs
     ) -> None:
         previous_value = record.state
         record.state = value
@@ -67,7 +67,7 @@ def set_state(
             )
 
     def set_workflow(
-        self, identity, record, new_workflow_id, *args, uow=None, commit=True, **kwargs
+        self, identity, record, new_workflow_id, *args, uow=None, commit: bool=True, **kwargs
     ):
         if new_workflow_id not in current_oarepo_workflows.record_workflows:
             raise InvalidWorkflowError(
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index b4fe485..3cb9b86 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -3,7 +3,7 @@
 
 
 class RecordStateField(MappingSystemFieldMixin, SystemField):
-    def __init__(self, key="state", initial="draft", config=None) -> None:
+    def __init__(self, key: str="state", initial: str="draft", config=None) -> None:
         self._config = config
         self._initial = initial
         super().__init__(key=key)
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index c0fce77..c8135da 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -9,6 +9,7 @@
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.requests.policy import RecipientGeneratorMixin
 from oarepo_workflows.services.permissions.identity import auto_request_need
+from typing import Optional
 
 
 # invenio_records_permissions.generators.ConditionalGenerator._make_query
@@ -35,7 +36,7 @@ def _get_workflow_id(self, record=None, **kwargs):
                 raise MissingWorkflowError("Workflow not defined in input.")
         return workflow_id
 
-    def _get_permissions_from_workflow(self, record=None, action_name=None, **kwargs):
+    def _get_permissions_from_workflow(self, record=None, action_name: Optional[str]=None, **kwargs):
         workflow_id = self._get_workflow_id(record, **kwargs)
         if workflow_id not in current_oarepo_workflows.record_workflows:
             raise InvalidWorkflowError(
diff --git a/oarepo_workflows/services/permissions/policy.py b/oarepo_workflows/services/permissions/policy.py
index 8e51f77..06d41c3 100644
--- a/oarepo_workflows/services/permissions/policy.py
+++ b/oarepo_workflows/services/permissions/policy.py
@@ -12,6 +12,7 @@
 
 from ...proxies import current_oarepo_workflows
 from .generators import IfInState, SameAs, WorkflowPermission
+from typing import Optional
 
 
 class DefaultWorkflowPermissions(RecordPermissionPolicy):
@@ -37,7 +38,7 @@ class MyWorkflowPermissions(DefaultWorkflowPermissions):
 
     system_process = SystemProcess()
 
-    def __init__(self, action_name=None, **over) -> None:
+    def __init__(self, action_name: Optional[str]=None, **over) -> None:
         can = getattr(self, f"can_{action_name}")
         if self.system_process not in can:
             can.append(self.system_process)

From d803f3a78672bdfba5bb1b9547242c4e9987f7fc Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 15:29:26 +0100
Subject: [PATCH 04/21] Typing 3rd round

---
 oarepo_workflows/ext.py                           |  8 ++++----
 oarepo_workflows/records/systemfields/state.py    |  6 ++++--
 oarepo_workflows/records/systemfields/workflow.py |  2 +-
 oarepo_workflows/requests/events.py               |  7 ++++---
 oarepo_workflows/requests/policy.py               |  8 ++++----
 .../resolvers/auto_approve/__init__.py            |  8 ++++----
 oarepo_workflows/services/components/workflow.py  |  2 +-
 .../services/permissions/generators.py            | 15 ++++++++-------
 oarepo_workflows/services/permissions/policy.py   |  2 +-
 oarepo_workflows/views/api.py                     |  3 ++-
 oarepo_workflows/views/app.py                     |  3 ++-
 11 files changed, 35 insertions(+), 29 deletions(-)

diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 310ef4d..4e9d363 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -40,14 +40,14 @@ def init_services(self, app) -> None:
         )
 
     @cached_property
-    def state_changed_notifiers(self):
+    def state_changed_notifiers(self) -> list:
         group_name = "oarepo_workflows.state_changed_notifiers"
         return [
             x.load() for x in importlib_metadata.entry_points().select(group=group_name)
         ]
 
     @cached_property
-    def workflow_changed_notifiers(self):
+    def workflow_changed_notifiers(self) -> list:
         group_name = "oarepo_workflows.workflow_changed_notifiers"
         return [
             x.load() for x in importlib_metadata.entry_points().select(group=group_name)
@@ -68,7 +68,7 @@ def set_state(
 
     def set_workflow(
         self, identity, record, new_workflow_id, *args, uow=None, commit: bool=True, **kwargs
-    ):
+    ) -> None:
         if new_workflow_id not in current_oarepo_workflows.record_workflows:
             raise InvalidWorkflowError(
                 f"Workflow {new_workflow_id} does not exist in the configuration."
@@ -106,7 +106,7 @@ def record_workflows(self):
         return self.app.config["WORKFLOWS"]
 
     @property
-    def default_workflow_event_submitters(self):
+    def default_workflow_event_submitters(self) -> dict:
         if "DEFAULT_WORKFLOW_EVENT_SUBMITTERS" in self.app.config:
             return self.app.config["DEFAULT_WORKFLOW_EVENT_SUBMITTERS"]
         else:
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index 3cb9b86..edbd6f3 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -1,6 +1,8 @@
+from __future__ import annotations
 from invenio_records.systemfields.base import SystemField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
 
+from typing import Self
 
 class RecordStateField(MappingSystemFieldMixin, SystemField):
     def __init__(self, key: str="state", initial: str="draft", config=None) -> None:
@@ -15,7 +17,7 @@ def post_init(self, record, data, model=None, **kwargs) -> None:
         if not record.state:
             self.set_dictkey(record, self._initial)
 
-    def __get__(self, record, owner=None):
+    def __get__(self, record, owner=None) -> str | Self:
         """Get the persistent identifier."""
         if record is None:
             return self
@@ -25,5 +27,5 @@ def __set__(self, record, value) -> None:
         self.set_dictkey(record, value)
 
     @property
-    def mapping(self):
+    def mapping(self) -> dict[str, dict[str, str]]:
         return {self.attr_name: {"type": "keyword"}}
diff --git a/oarepo_workflows/records/systemfields/workflow.py b/oarepo_workflows/records/systemfields/workflow.py
index 0a9b761..19caea1 100644
--- a/oarepo_workflows/records/systemfields/workflow.py
+++ b/oarepo_workflows/records/systemfields/workflow.py
@@ -9,5 +9,5 @@ def __init__(self) -> None:
         super().__init__(model_field_name="workflow", key="workflow")
 
     @property
-    def mapping(self):
+    def mapping(self) -> dict[str, dict[str, str]]:
         return {self.attr_name: {"type": "keyword"}}
diff --git a/oarepo_workflows/requests/events.py b/oarepo_workflows/requests/events.py
index b558dae..c224294 100644
--- a/oarepo_workflows/requests/events.py
+++ b/oarepo_workflows/requests/events.py
@@ -1,6 +1,7 @@
 import dataclasses
 from typing import List, Tuple
 
+from flask_principal import Need
 from invenio_records_permissions.generators import Generator
 
 
@@ -8,19 +9,19 @@
 class WorkflowEvent:
     submitters: List[Generator] | Tuple[Generator]
 
-    def needs(self, **kwargs):
+    def needs(self, **kwargs) -> set[Need]:
         return {
             need for generator in self.submitters for need in generator.needs(**kwargs)
         }
 
-    def excludes(self, **kwargs):
+    def excludes(self, **kwargs) -> set[Need]:
         return {
             exclude
             for generator in self.submitters
             for exclude in generator.excludes(**kwargs)
         }
 
-    def query_filters(self, **kwargs):
+    def query_filters(self, **kwargs) -> list[dict]:
         return [
             query_filter
             for generator in self.submitters
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index a9cc68b..ceba3fb 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -149,7 +149,7 @@ def __getitem__(self, item):
             )
 
     @cached_property
-    def items(self):
+    def items(self) -> list[tuple[str, WorkflowRequest]]:
         ret = []
         parent_attrs = set(dir(WorkflowRequestPolicy))
         for attr in dir(self.__class__):
@@ -162,7 +162,7 @@ def items(self):
                 ret.append((attr, possible_request))
         return ret
 
-    def applicable_workflow_requests(self, identity, **context):
+    def applicable_workflow_requests(self, identity, **context) -> list[tuple[str, WorkflowRequest]]:
         """
         Return a list of applicable requests for the identity and context.
         """
@@ -184,7 +184,7 @@ class AutoRequest(Generator):
     when a record is moved to a specific state.
     """
 
-    def needs(self, **kwargs):
+    def needs(self, **kwargs) -> list[Need]:
         """Enabling Needs."""
         return [auto_request_need]
 
@@ -213,7 +213,7 @@ class AutoApprove(RecipientGeneratorMixin, Generator):
     the request will be automatically approved when the request is submitted.
     """
 
-    def reference_receivers(self, record=None, request_type=None, **kwargs):
+    def reference_receivers(self, record=None, request_type=None, **kwargs) -> list[dict[str, str]]:
         return [{"auto_approve": "true"}]
 
 
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index 45fd471..5d09006 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -29,14 +29,14 @@ def __init__(self) -> None:
             "auto_approve",
         )
 
-    def matches_reference_dict(self, ref_dict):
+    def matches_reference_dict(self, ref_dict) -> bool:
         return self._parse_ref_dict_type(ref_dict) == self.type_id
 
-    def _reference_entity(self, entity):
+    def _reference_entity(self, entity) -> dict[str, str]:
         return {self.type_key: str(entity.value)}
 
-    def matches_entity(self, entity):
+    def matches_entity(self, entity) -> bool:
         return isinstance(entity, AutoApprover)
 
-    def _get_entity_proxy(self, ref_dict):
+    def _get_entity_proxy(self, ref_dict) -> AutoApproveProxy:
         return AutoApproveProxy(self, ref_dict)
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 70d7fb1..5fc9fdd 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -5,7 +5,7 @@
 
 
 class WorkflowComponent(ServiceComponent):
-    def create(self, identity, data=None, record=None, **kwargs):
+    def create(self, identity, data=None, record=None, **kwargs) -> None:
         try:
             workflow_id = data["parent"]["workflow"]
         except KeyError:
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index c8135da..c9c159b 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -10,10 +10,11 @@
 from oarepo_workflows.requests.policy import RecipientGeneratorMixin
 from oarepo_workflows.services.permissions.identity import auto_request_need
 from typing import Optional
+from flask import Need
 
 
 # invenio_records_permissions.generators.ConditionalGenerator._make_query
-def _make_query(generators, **kwargs):
+def _make_query(generators, **kwargs) -> dict:
     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
@@ -59,7 +60,7 @@ def __init__(self, state, then_) -> None:
         super().__init__(then_, else_=[])
         self.state = state
 
-    def _condition(self, record, **kwargs):
+    def _condition(self, record, **kwargs) -> bool:
         try:
             state = record.state
         except AttributeError:
@@ -83,14 +84,14 @@ def __init__(self, as_) -> None:
     def _generators(self, policy, **kwargs):
         return getattr(policy, self.as_)
 
-    def needs(self, policy, **kwargs):
+    def needs(self, policy, **kwargs) -> set[Need]:
         needs = [
             generator.needs(**kwargs)
             for generator in self._generators(policy, **kwargs)
         ]
         return set(chain.from_iterable(needs))
 
-    def excludes(self, policy, **kwargs):
+    def excludes(self, policy, **kwargs) -> set[Need]:
         """Preventing Needs."""
         excludes = [
             generator.excludes(**kwargs)
@@ -98,7 +99,7 @@ def excludes(self, policy, **kwargs):
         ]
         return set(chain.from_iterable(excludes))
 
-    def query_filter(self, policy, **kwargs):
+    def query_filter(self, policy, **kwargs) -> dict:
         """Search filters."""
         return _make_query(self._generators(policy, **kwargs), **kwargs)
 
@@ -109,7 +110,7 @@ class AutoRequest(Generator):
     when a record is moved to a specific state.
     """
 
-    def needs(self, **kwargs):
+    def needs(self, **kwargs) -> list[Need]:
         """Enabling Needs."""
         return [auto_request_need]
 
@@ -120,5 +121,5 @@ class AutoApprove(RecipientGeneratorMixin, Generator):
     the request will be automatically approved when the request is submitted.
     """
 
-    def reference_receivers(self, record=None, request_type=None, **kwargs):
+    def reference_receivers(self, record=None, request_type=None, **kwargs) -> list[dict[str, str]]:
         return [{"auto_approve": "true"}]
diff --git a/oarepo_workflows/services/permissions/policy.py b/oarepo_workflows/services/permissions/policy.py
index 06d41c3..1cb75b4 100644
--- a/oarepo_workflows/services/permissions/policy.py
+++ b/oarepo_workflows/services/permissions/policy.py
@@ -102,7 +102,7 @@ class WorkflowPermissionPolicy(RecordPermissionPolicy):
     can_search_versions = [SystemProcess(), AnyUser()]
 
     @property
-    def query_filters(self):
+    def query_filters(self) -> list[dict]:
         if not (self.action == "read" or self.action == "read_draft"):
             return super().query_filters
         workflows = current_oarepo_workflows.record_workflows
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index 08a7625..ffc2b7d 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -1,6 +1,7 @@
 from flask import Blueprint
 
-def create_api_blueprint(app):
+from flask.blueprints import Blueprint
+def create_api_blueprint(app) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index 9f0f387..82e58ef 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -1,6 +1,7 @@
 from flask import Blueprint
 
-def create_app_blueprint(app):
+from flask.blueprints import Blueprint
+def create_app_blueprint(app) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 

From 85c9c4ce7f5aac1ae8ab6179aa92d8868695fb8a Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 15:30:25 +0100
Subject: [PATCH 05/21] isort

---
 oarepo_workflows/__init__.py                  | 23 ++++++++-----------
 oarepo_workflows/base.py                      |  2 ++
 oarepo_workflows/errors.py                    |  2 ++
 oarepo_workflows/ext.py                       |  6 +++--
 oarepo_workflows/ext_config.py                |  5 +++-
 oarepo_workflows/proxies.py                   |  2 ++
 oarepo_workflows/records/models.py            |  2 ++
 .../records/systemfields/state.py             |  4 +++-
 .../records/systemfields/workflow.py          |  2 ++
 oarepo_workflows/requests/__init__.py         | 17 +++++---------
 oarepo_workflows/requests/events.py           |  2 ++
 oarepo_workflows/requests/policy.py           |  4 ++--
 .../resolvers/auto_approve/__init__.py        |  7 ++++--
 .../services/auto_approve/__init__.py         |  2 ++
 .../services/components/workflow.py           |  5 +++-
 .../services/permissions/__init__.py          |  2 ++
 .../services/permissions/generators.py        |  9 +++++---
 .../services/permissions/identity.py          |  2 ++
 .../services/permissions/policy.py            | 12 ++++------
 oarepo_workflows/services/records/schema.py   |  2 ++
 oarepo_workflows/views/api.py                 |  5 +++-
 oarepo_workflows/views/app.py                 |  5 +++-
 22 files changed, 76 insertions(+), 46 deletions(-)

diff --git a/oarepo_workflows/__init__.py b/oarepo_workflows/__init__.py
index 5d973e1..2b95ad1 100644
--- a/oarepo_workflows/__init__.py
+++ b/oarepo_workflows/__init__.py
@@ -1,19 +1,14 @@
-from oarepo_workflows.services.permissions import (
-    AutoApprove,
-    AutoRequest,
-    DefaultWorkflowPermissions,
-    IfInState,
-    WorkflowPermission,
-    WorkflowPermissionPolicy,
-)
+from __future__ import annotations
+
+from oarepo_workflows.services.permissions import (AutoApprove, AutoRequest,
+                                                   DefaultWorkflowPermissions,
+                                                   IfInState,
+                                                   WorkflowPermission,
+                                                   WorkflowPermissionPolicy)
 
 from .base import Workflow
-from .requests import (
-    WorkflowRequest,
-    WorkflowRequestEscalation,
-    WorkflowRequestPolicy,
-    WorkflowTransitions,
-)
+from .requests import (WorkflowRequest, WorkflowRequestEscalation,
+                       WorkflowRequestPolicy, WorkflowTransitions)
 
 __all__ = (
     "IfInState",
diff --git a/oarepo_workflows/base.py b/oarepo_workflows/base.py
index 0026f39..ab9ca7b 100644
--- a/oarepo_workflows/base.py
+++ b/oarepo_workflows/base.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import dataclasses
 from typing import Type
 
diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index 9a26ab6..d489c2b 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from marshmallow import ValidationError
 
 
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 4e9d363..885e6e0 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from functools import cached_property
 
 import importlib_metadata
@@ -7,8 +9,8 @@
 
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
-from oarepo_workflows.services.auto_approve import AutoApproveEntityService, \
-    AutoApproveEntityServiceConfig
+from oarepo_workflows.services.auto_approve import (
+    AutoApproveEntityService, AutoApproveEntityServiceConfig)
 
 
 class OARepoWorkflows(object):
diff --git a/oarepo_workflows/ext_config.py b/oarepo_workflows/ext_config.py
index 5f70579..111a928 100644
--- a/oarepo_workflows/ext_config.py
+++ b/oarepo_workflows/ext_config.py
@@ -1,4 +1,7 @@
-from oarepo_workflows.services.permissions.policy import WorkflowPermissionPolicy
+from __future__ import annotations
+
+from oarepo_workflows.services.permissions.policy import \
+    WorkflowPermissionPolicy
 
 OAREPO_PERMISSIONS_PRESETS = {
     "workflow": WorkflowPermissionPolicy,
diff --git a/oarepo_workflows/proxies.py b/oarepo_workflows/proxies.py
index 323e5f3..15c86ec 100644
--- a/oarepo_workflows/proxies.py
+++ b/oarepo_workflows/proxies.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from flask import current_app
 from werkzeug.local import LocalProxy
 
diff --git a/oarepo_workflows/records/models.py b/oarepo_workflows/records/models.py
index b71b183..e7da3ec 100644
--- a/oarepo_workflows/records/models.py
+++ b/oarepo_workflows/records/models.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from invenio_db import db
 from sqlalchemy import String
 
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index edbd6f3..06bd03c 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -1,8 +1,10 @@
 from __future__ import annotations
+
+from typing import Self
+
 from invenio_records.systemfields.base import SystemField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
 
-from typing import Self
 
 class RecordStateField(MappingSystemFieldMixin, SystemField):
     def __init__(self, key: str="state", initial: str="draft", config=None) -> None:
diff --git a/oarepo_workflows/records/systemfields/workflow.py b/oarepo_workflows/records/systemfields/workflow.py
index 19caea1..43a1d2b 100644
--- a/oarepo_workflows/records/systemfields/workflow.py
+++ b/oarepo_workflows/records/systemfields/workflow.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from invenio_records.systemfields.model import ModelField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
 
diff --git a/oarepo_workflows/requests/__init__.py b/oarepo_workflows/requests/__init__.py
index 3f48891..d638c5c 100644
--- a/oarepo_workflows/requests/__init__.py
+++ b/oarepo_workflows/requests/__init__.py
@@ -1,14 +1,9 @@
-from .policy import (
-    RecipientGeneratorMixin,
-    WorkflowRequest,
-    WorkflowRequestEscalation,
-    WorkflowRequestPolicy,
-    WorkflowTransitions,
-    AutoApprove,
-    AutoRequest,
-    RequesterGenerator,
-    RecipientEntityReference
-)
+from __future__ import annotations
+
+from .policy import (AutoApprove, AutoRequest, RecipientEntityReference,
+                     RecipientGeneratorMixin, RequesterGenerator,
+                     WorkflowRequest, WorkflowRequestEscalation,
+                     WorkflowRequestPolicy, WorkflowTransitions)
 
 __all__ = (
     "WorkflowRequestPolicy",
diff --git a/oarepo_workflows/requests/events.py b/oarepo_workflows/requests/events.py
index c224294..a6188c2 100644
--- a/oarepo_workflows/requests/events.py
+++ b/oarepo_workflows/requests/events.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import dataclasses
 from typing import List, Tuple
 
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index ceba3fb..b83c02c 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -1,12 +1,12 @@
 from __future__ import annotations
 
 import dataclasses
-from functools import cached_property
 from datetime import timedelta
+from functools import cached_property
 from logging import getLogger
 from typing import Dict, List, Optional, Tuple
 
-from flask_principal import Need, Permission, Identity
+from flask_principal import Identity, Need, Permission
 from invenio_access.permissions import SystemRoleNeed
 from invenio_records_permissions.generators import Generator
 
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index 5d09006..cd5950c 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -1,6 +1,9 @@
-from invenio_records_resources.references.entity_resolvers import EntityProxy
-from invenio_records_resources.references.entity_resolvers.base import EntityResolver
+from __future__ import annotations
+
 from flask_principal import Need
+from invenio_records_resources.references.entity_resolvers import EntityProxy
+from invenio_records_resources.references.entity_resolvers.base import \
+    EntityResolver
 
 
 class AutoApprover:
diff --git a/oarepo_workflows/services/auto_approve/__init__.py b/oarepo_workflows/services/auto_approve/__init__.py
index 43a43ad..720e698 100644
--- a/oarepo_workflows/services/auto_approve/__init__.py
+++ b/oarepo_workflows/services/auto_approve/__init__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from oarepo_runtime.services.entity.config import KeywordEntityServiceConfig
 from oarepo_runtime.services.entity.service import KeywordEntityService
 
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 5fc9fdd..1829014 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -1,4 +1,7 @@
-from invenio_records_resources.services.records.components.base import ServiceComponent
+from __future__ import annotations
+
+from invenio_records_resources.services.records.components.base import \
+    ServiceComponent
 
 from oarepo_workflows.errors import MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
diff --git a/oarepo_workflows/services/permissions/__init__.py b/oarepo_workflows/services/permissions/__init__.py
index 09499f4..8fb5c14 100644
--- a/oarepo_workflows/services/permissions/__init__.py
+++ b/oarepo_workflows/services/permissions/__init__.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from .generators import AutoApprove, AutoRequest, IfInState, WorkflowPermission
 from .policy import DefaultWorkflowPermissions, WorkflowPermissionPolicy
 
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index c9c159b..a36016b 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -1,16 +1,19 @@
+from __future__ import annotations
+
 import operator
 from functools import reduce
 from itertools import chain
+from typing import Optional
 
-from invenio_records_permissions.generators import ConditionalGenerator, Generator
+from flask import Need
+from invenio_records_permissions.generators import (ConditionalGenerator,
+                                                    Generator)
 from invenio_search.engine import dsl
 
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.requests.policy import RecipientGeneratorMixin
 from oarepo_workflows.services.permissions.identity import auto_request_need
-from typing import Optional
-from flask import Need
 
 
 # invenio_records_permissions.generators.ConditionalGenerator._make_query
diff --git a/oarepo_workflows/services/permissions/identity.py b/oarepo_workflows/services/permissions/identity.py
index 767201d..89da89b 100644
--- a/oarepo_workflows/services/permissions/identity.py
+++ b/oarepo_workflows/services/permissions/identity.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 from invenio_access.permissions import SystemRoleNeed
 
 auto_request_need = SystemRoleNeed("auto_request")
diff --git a/oarepo_workflows/services/permissions/policy.py b/oarepo_workflows/services/permissions/policy.py
index 1cb75b4..37dc2a3 100644
--- a/oarepo_workflows/services/permissions/policy.py
+++ b/oarepo_workflows/services/permissions/policy.py
@@ -1,18 +1,16 @@
+from __future__ import annotations
+
 from functools import reduce
+from typing import Optional
 
 from invenio_records_permissions import RecordPermissionPolicy
-from invenio_records_permissions.generators import (
-    AnyUser,
-    AuthenticatedUser,
-    Disable,
-    SystemProcess,
-)
+from invenio_records_permissions.generators import (AnyUser, AuthenticatedUser,
+                                                    Disable, SystemProcess)
 from invenio_search.engine import dsl
 from oarepo_runtime.services.generators import RecordOwners
 
 from ...proxies import current_oarepo_workflows
 from .generators import IfInState, SameAs, WorkflowPermission
-from typing import Optional
 
 
 class DefaultWorkflowPermissions(RecordPermissionPolicy):
diff --git a/oarepo_workflows/services/records/schema.py b/oarepo_workflows/services/records/schema.py
index afac455..5c8f137 100644
--- a/oarepo_workflows/services/records/schema.py
+++ b/oarepo_workflows/services/records/schema.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
 import marshmallow as ma
 from invenio_drafts_resources.services.records.schema import ParentSchema
 
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index ffc2b7d..d62ea5c 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -1,6 +1,9 @@
-from flask import Blueprint
+from __future__ import annotations
 
+from flask import Blueprint
 from flask.blueprints import Blueprint
+
+
 def create_api_blueprint(app) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index 82e58ef..969eede 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -1,6 +1,9 @@
-from flask import Blueprint
+from __future__ import annotations
 
+from flask import Blueprint
 from flask.blueprints import Blueprint
+
+
 def create_app_blueprint(app) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)

From ed8c0639d93f8c814a8281efdc1ef662c4c105c6 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 20:59:32 +0100
Subject: [PATCH 06/21] Working on documentation and type annotations

---
 .copyright.tmpl                               |   5 +
 annotations.json                              | 348 ++++++++++++++++++
 mypy.ini                                      |   2 +
 oarepo_workflows/__init__.py                  |  36 +-
 oarepo_workflows/base.py                      | 110 +++++-
 oarepo_workflows/errors.py                    |  66 +++-
 oarepo_workflows/ext.py                       | 189 +++++++---
 oarepo_workflows/ext_config.py                |  18 +-
 oarepo_workflows/proxies.py                   |  10 +
 oarepo_workflows/records/__init__.py          |   8 +
 oarepo_workflows/records/models.py            |  12 +
 .../records/systemfields/__init__.py          |  16 +
 .../records/systemfields/state.py             |  51 ++-
 .../records/systemfields/workflow.py          |  12 +
 oarepo_workflows/requests/__init__.py         |  34 +-
 oarepo_workflows/requests/events.py           |  51 +--
 oarepo_workflows/requests/generators.py       | 116 ++++++
 oarepo_workflows/requests/policy.py           | 278 ++------------
 oarepo_workflows/requests/requests.py         | 161 ++++++++
 oarepo_workflows/resolvers/__init__.py        |   8 +
 .../resolvers/auto_approve/__init__.py        |  65 ++--
 oarepo_workflows/services/__init__.py         |   8 +
 .../services/auto_approve/__init__.py         |  17 +
 .../services/components/__init__.py           |   8 +
 .../services/components/workflow.py           |  40 +-
 .../services/permissions/__init__.py          |  18 +-
 .../services/permissions/generators.py        | 201 ++++++----
 .../services/permissions/identity.py          |   5 -
 .../services/permissions/policy.py            | 117 ------
 .../permissions/record_permission_policy.py   |  71 ++++
 .../permissions/workflow_permissions.py       |  71 ++++
 oarepo_workflows/services/records/__init__.py |   7 +
 oarepo_workflows/services/records/schema.py   |   7 +
 oarepo_workflows/views/__init__.py            |   7 +
 oarepo_workflows/views/api.py                 |   8 +
 oarepo_workflows/views/app.py                 |   8 +
 ruff.toml                                     |  26 ++
 setup.cfg                                     |   2 +-
 setup.py                                      |   7 +
 tests/__init__.py                             |   7 +
 tests/conftest.py                             |   9 +-
 tests/test_workflow.py                        |   7 +
 tests/test_workflow_field.py                  |   7 +
 tests/test_workflow_requests.py               |  29 +-
 44 files changed, 1692 insertions(+), 591 deletions(-)
 create mode 100644 .copyright.tmpl
 create mode 100644 annotations.json
 create mode 100644 mypy.ini
 create mode 100644 oarepo_workflows/requests/generators.py
 create mode 100644 oarepo_workflows/requests/requests.py
 delete mode 100644 oarepo_workflows/services/permissions/identity.py
 delete mode 100644 oarepo_workflows/services/permissions/policy.py
 create mode 100644 oarepo_workflows/services/permissions/record_permission_policy.py
 create mode 100644 oarepo_workflows/services/permissions/workflow_permissions.py
 create mode 100644 ruff.toml

diff --git a/.copyright.tmpl b/.copyright.tmpl
new file mode 100644
index 0000000..6186113
--- /dev/null
+++ b/.copyright.tmpl
@@ -0,0 +1,5 @@
+Copyright (C) 2024 CESNET z.s.p.o.
+
+oarepo-workflows is free software; you can redistribute it and/or
+modify it under the terms of the MIT License; see LICENSE file for more
+details.
diff --git a/annotations.json b/annotations.json
new file mode 100644
index 0000000..6f39b8e
--- /dev/null
+++ b/annotations.json
@@ -0,0 +1,348 @@
+[
+    {
+        "path": "oarepo_workflows/base.py",
+        "line": 23,
+        "func_name": "Workflow.requests",
+        "type_comments": [
+            "() -> tests.conftest.MyWorkflowRequests"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/records/systemfields/state.py",
+        "line": 15,
+        "func_name": "RecordStateField.post_create",
+        "type_comments": [
+            "(thesis.thesis.records.api.ThesisRecord) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/records/systemfields/state.py",
+        "line": 18,
+        "func_name": "RecordStateField.post_init",
+        "type_comments": [
+            "(thesis.thesis.records.api.ThesisRecord, Dict[str, str], thesis.records.models.ThesisMetadata) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/records/systemfields/state.py",
+        "line": 22,
+        "func_name": "RecordStateField.__get__",
+        "type_comments": [
+            "(thesis.thesis.records.api.ThesisRecord, invenio_records.systemfields.base.SystemFieldsMeta) -> None"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 44,
+        "func_name": "requester_generator",
+        "type_comments": [
+            "() -> oarepo_workflows.requests.policy.RequesterGenerator"
+        ],
+        "samples": 3
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 51,
+        "func_name": "WorkflowRequest.recipient_entity_reference",
+        "type_comments": [
+            "() -> Dict[str, str]"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 61,
+        "func_name": "WorkflowRequest.is_applicable",
+        "type_comments": [
+            "(flask_principal.Identity) -> bool"
+        ],
+        "samples": 6
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 151,
+        "func_name": "items",
+        "type_comments": [
+            "() -> List[Tuple[str, oarepo_workflows.requests.policy.WorkflowRequest]]"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 165,
+        "func_name": "WorkflowRequestPolicy.applicable_workflow_requests",
+        "type_comments": [
+            "(flask_principal.Identity) -> List[Tuple[str, oarepo_workflows.requests.policy.WorkflowRequest]]"
+        ],
+        "samples": 2
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 221,
+        "func_name": "RequesterGenerator.__init__",
+        "type_comments": [
+            "(List[oarepo_runtime.services.permissions.generators.UserWithRole]) -> pyannotate_runtime.collect_types.NoReturnType",
+            "(List[oarepo_runtime.services.permissions.generators.RecordOwners]) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 3
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 225,
+        "func_name": "RequesterGenerator.needs",
+        "type_comments": [
+            "() -> Set[flask_principal.Need]"
+        ],
+        "samples": 6
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 237,
+        "func_name": "RequesterGenerator.excludes",
+        "type_comments": [
+            "() -> Set"
+        ],
+        "samples": 6
+    },
+    {
+        "path": "oarepo_workflows/requests/policy.py",
+        "line": 264,
+        "func_name": "RecipientEntityReference",
+        "type_comments": [
+            "(oarepo_workflows.requests.policy.WorkflowRequest) -> Dict[str, str]"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "oarepo_workflows/services/permissions/generators.py",
+        "line": 32,
+        "func_name": "WorkflowPermission._get_workflow_id",
+        "type_comments": [
+            "(None) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 10
+    },
+    {
+        "path": "oarepo_workflows/services/permissions/generators.py",
+        "line": 43,
+        "func_name": "WorkflowPermission._get_permissions_from_workflow",
+        "type_comments": [
+            "(None, str) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 10
+    },
+    {
+        "path": "oarepo_workflows/services/permissions/generators.py",
+        "line": 52,
+        "func_name": "WorkflowPermission.needs",
+        "type_comments": [
+            "(None) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 10
+    },
+    {
+        "path": "tests/conftest.py",
+        "line": 153,
+        "func_name": "LoggedClient.__init__",
+        "type_comments": [
+            "(flask.testing.FlaskClient, pytest_invenio.user.UserFixtureBase) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 7
+    },
+    {
+        "path": "tests/conftest.py",
+        "line": 157,
+        "func_name": "LoggedClient._login",
+        "type_comments": [
+            "() -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 5
+    },
+    {
+        "path": "tests/conftest.py",
+        "line": 161,
+        "func_name": "LoggedClient.post",
+        "type_comments": [
+            "(*str) -> werkzeug.test.WrapperTestResponse",
+            "(*str) -> werkzeug.test.WrapperTestResponse",
+            "(*str) -> werkzeug.test.WrapperTestResponse",
+            "(*str) -> werkzeug.test.WrapperTestResponse"
+        ],
+        "samples": 5
+    },
+    {
+        "path": "tests/conftest.py",
+        "line": 180,
+        "func_name": "_logged_client",
+        "type_comments": [
+            "(pytest_invenio.user.UserFixtureBase) -> tests.conftest.LoggedClient"
+        ],
+        "samples": 7
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 8,
+        "func_name": "test_workflow_read",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 42,
+        "func_name": "test_workflow_publish",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 67,
+        "func_name": "test_query_filter",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 98,
+        "func_name": "test_invalid_workflow_input",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 115,
+        "func_name": "test_state_change",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], werkzeug.local.LocalProxy, method, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 123,
+        "func_name": "test_set_workflow",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, method, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 142,
+        "func_name": "test_state_change_entrypoint_hookup",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], werkzeug.local.LocalProxy, method, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow.py",
+        "line": 150,
+        "func_name": "test_set_workflow_entrypoint_hookup",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, method, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_field.py",
+        "line": 4,
+        "func_name": "test_workflow_read",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 15,
+        "func_name": "TestRecipient.reference_receivers",
+        "type_comments": [
+            "(thesis.thesis.records.api.ThesisRecord, None) -> List[Dict[str, str]]"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 21,
+        "func_name": "NullRecipient.reference_receivers",
+        "type_comments": [
+            "(thesis.thesis.records.api.ThesisRecord, None) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 25,
+        "func_name": "test_workflow_requests",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 34,
+        "func_name": "test_request_policy_access",
+        "type_comments": [
+            "(invenio_app.factory:flask.Flask) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 39,
+        "func_name": "test_is_applicable",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 54,
+        "func_name": "test_list_applicable_requests",
+        "type_comments": [
+            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "tests/test_workflow_requests.py",
+        "line": 55,
+        "func_name": "R",
+        "type_comments": [
+            "() -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 1
+    },
+    {
+        "path": "thesis/thesis/resources/records/config.py",
+        "line": 14,
+        "func_name": "response_handlers",
+        "type_comments": [
+            "() -> Dict[str, flask_resources.responses.ResponseHandler]"
+        ],
+        "samples": 5
+    },
+    {
+        "path": "thesis/thesis/resources/records/ui.py",
+        "line": 12,
+        "func_name": "ThesisUIJSONSerializer.__init__",
+        "type_comments": [
+            "() -> pyannotate_runtime.collect_types.NoReturnType"
+        ],
+        "samples": 5
+    }
+]
\ No newline at end of file
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 0000000..336aa08
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,2 @@
+[mypy]
+disable_error_code = import-untyped, import-not-found
diff --git a/oarepo_workflows/__init__.py b/oarepo_workflows/__init__.py
index 2b95ad1..0621c00 100644
--- a/oarepo_workflows/__init__.py
+++ b/oarepo_workflows/__init__.py
@@ -1,25 +1,39 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Support any workflows on Invenio record."""
+
 from __future__ import annotations
 
-from oarepo_workflows.services.permissions import (AutoApprove, AutoRequest,
-                                                   DefaultWorkflowPermissions,
-                                                   IfInState,
-                                                   WorkflowPermission,
-                                                   WorkflowPermissionPolicy)
+from oarepo_workflows.services.permissions import (
+    IfInState,
+    WorkflowPermission,
+    WorkflowRecordPermissionPolicy,
+)
 
 from .base import Workflow
-from .requests import (WorkflowRequest, WorkflowRequestEscalation,
-                       WorkflowRequestPolicy, WorkflowTransitions)
+from .requests import (
+    AutoApprove,
+    AutoRequest,
+    WorkflowRequest,
+    WorkflowRequestEscalation,
+    WorkflowRequestPolicy,
+    WorkflowTransitions,
+)
 
 __all__ = (
     "IfInState",
     "Workflow",
     "WorkflowPermission",
-    "DefaultWorkflowPermissions",
-    "WorkflowPermissionPolicy",
+    "WorkflowRecordPermissionPolicy",
     "WorkflowRequestPolicy",
-    "WorkflowRequest",
-    "WorkflowTransitions",
     "AutoApprove",
     "AutoRequest",
+    "WorkflowRequest",
     "WorkflowRequestEscalation",
+    "WorkflowTransitions",
 )
diff --git a/oarepo_workflows/base.py b/oarepo_workflows/base.py
index ab9ca7b..063e74e 100644
--- a/oarepo_workflows/base.py
+++ b/oarepo_workflows/base.py
@@ -1,31 +1,119 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Support any workflows on Invenio record."""
+
 from __future__ import annotations
 
 import dataclasses
-from typing import Type
-
-from flask_babel import LazyString
+from typing import TYPE_CHECKING, Any, Protocol
 
-from . import WorkflowPermissionPolicy
+from . import WorkflowRecordPermissionPolicy
 from .requests import WorkflowRequestPolicy
 from .services.permissions import DefaultWorkflowPermissions
 
+if TYPE_CHECKING:
+    from flask_babel import LazyString
+    from flask_principal import Identity
+    from invenio_records_resources.records import Record
+    from invenio_records_resources.services.uow import UnitOfWork
+
 
 @dataclasses.dataclass
 class Workflow:
+    """A workflow definition."""
+
     label: str | LazyString
-    permission_policy_cls: Type[DefaultWorkflowPermissions]
-    request_policy_cls: Type[WorkflowRequestPolicy] = WorkflowRequestPolicy
+    """A human-readable label for the workflow."""
+
+    permission_policy_cls: type[DefaultWorkflowPermissions]
+    """A permission policy class that handles permissions on records that are governed by this workflow."""
 
-    def permissions(self, action, **over):
-        """Return permission policy for this workflow applicable to the given action."""
+    request_policy_cls: type[WorkflowRequestPolicy] = WorkflowRequestPolicy
+    """A request policy class that defines which requests can be applied to records governed by this workflow."""
+
+    def permissions(self, action: str, **over: Any) -> DefaultWorkflowPermissions:
+        """Return permission policy for this workflow applicable to the given action.
+
+        :param  action:      action for which permission is asked
+        """
         return self.permission_policy_cls(action, **over)
 
-    def requests(self):
+    def requests(self) -> WorkflowRequestPolicy:
         """Return instance of request policy for this workflow."""
         return self.request_policy_cls()
 
     def __post_init__(self) -> None:
-        assert not issubclass(self.permission_policy_cls, WorkflowPermissionPolicy)
-        assert issubclass(self.permission_policy_cls, DefaultWorkflowPermissions)
+        """Check that the classes are subclasses of the expected classes.
 
+        This is just a sanity check to raise an error as soon as possible.
+        """
+        assert not issubclass(
+            self.permission_policy_cls, WorkflowRecordPermissionPolicy
+        )
+        assert issubclass(self.permission_policy_cls, DefaultWorkflowPermissions)
         assert issubclass(self.request_policy_cls, WorkflowRequestPolicy)
+
+
+class StateChangedNotifier(Protocol):
+    """A protocol for a state change notifier.
+
+    State changed notifier is a callable that is called when a state of a record changes,
+    for example as a result of a workflow transition.
+    """
+
+    def __call__(
+        self,
+        identity: Identity,
+        record: Record,
+        previous_state: str,
+        new_state: str,
+        *args: Any,
+        uow: UnitOfWork,
+        **kwargs: Any,
+    ):
+        """Notify about a state change.
+
+        :param identity:        identity of the user who initiated the state change
+        :param record:          record whose state changed
+        :param previous_state:  previous state of the record
+        :param new_state:       new state of the record
+        :param args:            additional arguments
+        :param uow:             unit of work
+        :param kwargs:          additional keyword arguments
+        """
+        ...
+
+
+class WorkflowChangeNotifier(Protocol):
+    """A protocol for a workflow change notifier.
+
+    Workflow changed notifier is a callable that is called
+    when a workflow of a record changes.
+    """
+
+    def __call__(
+        self,
+        identity: Identity,
+        record: Record,
+        previous_workflow_id: str,
+        new_workflow_id: str,
+        *args: Any,
+        uow: UnitOfWork,
+        **kwargs: Any,
+    ):
+        """Notify about a workflow change.
+
+        :param identity:                identity of the user who initiated the workflow change
+        :param record:                  record whose workflow changed
+        :param previous_workflow_id:    previous workflow of the record
+        :param new_workflow_id:         new workflow of the record
+        :param args:                    additional arguments
+        :param uow:                     unit of work
+        :param kwargs:                  additional keyword arguments
+        """
+        ...
diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index d489c2b..c211771 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -1,21 +1,67 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Errors raised by oarepo-workflows."""
+
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from marshmallow import ValidationError
 
+if TYPE_CHECKING:
+    from invenio_records_resources.records import Record
 
-class MissingWorkflowError(ValidationError):
+
+def _get_id_from_record(record: Record | dict) -> str:
+    """Get the id from a record.
+
+    :param record: A record or a dict representing a record.
+    :return str: The id of the record.
     """
-    Exception raised when a required workflow is missing.
-    Attributes:
-        message -- explanation of the error
+    # community record doesn't have id in dict form, only uuid
+    return str(record["id"]) if "id" in record else str(record.id)
+
+
+def _format_record(record: Record | dict) -> str:
+    """Format a record for error messages.
+
+    :param record: A record or a dict representing a record.
+    :return str: A formatted string representing the record.
     """
-    
+    return f"{type(record).__name__}[{_get_id_from_record(record)}]"
+
+
+class MissingWorkflowError(ValidationError):
+    """Exception raised when a required workflow is missing."""
+
+    def __init__(self, message: str, record: Record | dict | None = None) -> None:
+        """Initialize the exception."""
+        self.record = record
+        if record:
+            super().__init__(f"{message} Used on record {_format_record(record)}")
+        else:
+            super().__init__(message)
 
 
 class InvalidWorkflowError(ValidationError):
-    """
-    Exception raised when a workflow is invalid.
-    Attributes:
-        message -- explanation of the error
-    """
+    """Exception raised when a workflow is invalid."""
 
+    def __init__(
+        self,
+        message: str,
+        record: Record | dict | None = None,
+        community_id: str = None,
+    ) -> None:
+        """Initialize the exception."""
+        self.record = record
+        if record:
+            super().__init__(f"{message} Used on record {_format_record(record)}")
+        elif community_id:
+            super().__init__(f"{message} Used on community {community_id}")
+        else:
+            super().__init__(message)
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 885e6e0..96518a4 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -1,6 +1,16 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Flask extension for workflows."""
+
 from __future__ import annotations
 
 from functools import cached_property
+from typing import TYPE_CHECKING, Any
 
 import importlib_metadata
 from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
@@ -10,19 +20,43 @@
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.services.auto_approve import (
-    AutoApproveEntityService, AutoApproveEntityServiceConfig)
+    AutoApproveEntityService,
+    AutoApproveEntityServiceConfig,
+)
+
+if TYPE_CHECKING:
+    from flask import Flask
+    from flask_principal import Identity
+    from invenio_drafts_resources.records import ParentRecord
+    from invenio_records_resources.records import Record
+    from invenio_records_resources.services.uow import UnitOfWork
+
+    from oarepo_workflows.base import (
+        StateChangedNotifier,
+        Workflow,
+        WorkflowChangeNotifier,
+    )
+
 
+class OARepoWorkflows:
+    """OARepo workflows extension."""
 
-class OARepoWorkflows(object):
+    def __init__(self, app: Flask = None) -> None:
+        """Initialize the extension.
 
-    def __init__(self, app=None) -> None:
+        :param app: Flask application to initialize with.
+        If not passed here, it can be passed later using init_app method.
+        """
         if app:
             self.init_config(app)
             self.init_app(app)
             self.init_services(app)
 
-    def init_config(self, app) -> None:
-        """Initialize configuration."""
+    def init_config(self, app: Flask) -> None:
+        """Initialize configuration.
+
+        :param app: Flask application to initialize with.
+        """
         from . import ext_config
 
         if "OAREPO_PERMISSIONS_PRESETS" not in app.config:
@@ -36,53 +70,100 @@ def init_config(self, app) -> None:
 
         app.config.setdefault("WORKFLOWS", ext_config.WORKFLOWS)
 
-    def init_services(self, app) -> None:
+    def init_services(self) -> None:
+        """Initialize workflow services."""
         self.autoapprove_service = AutoApproveEntityService(
             config=AutoApproveEntityServiceConfig()
         )
 
     @cached_property
-    def state_changed_notifiers(self) -> list:
+    def state_changed_notifiers(self) -> list[StateChangedNotifier]:
+        """Return a list of state changed notifiers.
+
+        State changed notifiers are callables that are called when a state of a record changes,
+        for example as a result of a workflow transition.
+
+        They are registered as entry points in the group `oarepo_workflows.state_changed_notifiers`.
+        """
         group_name = "oarepo_workflows.state_changed_notifiers"
         return [
             x.load() for x in importlib_metadata.entry_points().select(group=group_name)
         ]
 
     @cached_property
-    def workflow_changed_notifiers(self) -> list:
+    def workflow_changed_notifiers(self) -> list[WorkflowChangeNotifier]:
+        """Return a list of workflow changed notifiers.
+
+        Workflow changed notifiers are callables that are called when a workflow of a record changes.
+        They are registered as entry points in the group `oarepo_workflows.workflow_changed_notifiers`.
+        """
         group_name = "oarepo_workflows.workflow_changed_notifiers"
         return [
             x.load() for x in importlib_metadata.entry_points().select(group=group_name)
         ]
 
     def set_state(
-        self, identity, record, value, *args, uow=None, commit: bool=True, **kwargs
+        self,
+        identity: Identity,
+        record: Record,
+        new_state: str,
+        *args: Any,
+        uow: UnitOfWork,
+        commit: bool = True,
+        **kwargs: Any,
     ) -> None:
+        """Set a new state on a record.
+
+        :param identity:    identity of the user who initiated the state change
+        :param record:      record whose state is being changed
+        :param new_state:   new state to set
+        :param args:        additional arguments
+        :param uow:         unit of work
+        :param commit:      whether to commit the change
+        :param kwargs:      additional keyword arguments
+        """
         previous_value = record.state
-        record.state = value
+        record.state = new_state
         if commit:
             service = get_record_service_for_record(record)
             uow.register(RecordCommitOp(record, indexer=service.indexer))
         for state_changed_notifier in self.state_changed_notifiers:
             state_changed_notifier(
-                identity, record, previous_value, value, *args, uow=uow, **kwargs
+                identity, record, previous_value, new_state, *args, uow=uow, **kwargs
             )
 
     def set_workflow(
-        self, identity, record, new_workflow_id, *args, uow=None, commit: bool=True, **kwargs
+        self,
+        identity: Identity,
+        record: Record,
+        new_workflow_id: str,
+        *args: Any,
+        uow: UnitOfWork,
+        commit: bool = True,
+        **kwargs: Any,
     ) -> None:
+        """Set a new workflow on a record.
+
+        :param identity:            identity of the user who initiated the workflow change
+        :param record:              record whose workflow is being changed
+        :param new_workflow_id:     new workflow to set
+        :param args:                additional arguments
+        :param uow:                 unit of work
+        :param commit:              whether to commit the change
+        :param kwargs:              additional keyword arguments
+        """
         if new_workflow_id not in current_oarepo_workflows.record_workflows:
             raise InvalidWorkflowError(
-                f"Workflow {new_workflow_id} does not exist in the configuration."
+                f"Workflow {new_workflow_id} does not exist in the configuration.",
+                record=record,
             )
-        previous_value = record.parent.workflow
-        record.parent.workflow = new_workflow_id
+        parent = record.parent  # noqa for typing: we do not have a better type for record with parent
+        previous_value = parent.workflow
+        parent.workflow = new_workflow_id
         if commit:
             service = get_record_service_for_record(record)
             uow.register(
-                ParentRecordCommitOp(
-                    record.parent, indexer_context=dict(service=service)
-                )
+                ParentRecordCommitOp(parent, indexer_context=dict(service=service))
             )
         for workflow_changed_notifier in self.workflow_changed_notifiers:
             workflow_changed_notifier(
@@ -95,47 +176,64 @@ def set_workflow(
                 **kwargs,
             )
 
-    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
+    def get_workflow_from_record(self, record: Record | ParentRecord) -> str | None:
+        """Get the workflow from a record.
+
+        :param record:  record to get the workflow from. Can pass either a record or its parent.
+        """
+        parent: ParentRecord = record.parent if hasattr(record, "parent") else record
+
+        if hasattr(parent, "workflow") and parent.workflow:
+            return parent.workflow
         else:
             return None
 
     @property
-    def record_workflows(self):
+    def record_workflows(self) -> dict[str, Workflow]:
+        """Return a dictionary of available record workflows."""
         return self.app.config["WORKFLOWS"]
 
     @property
-    def default_workflow_event_submitters(self) -> dict:
-        if "DEFAULT_WORKFLOW_EVENT_SUBMITTERS" in self.app.config:
-            return self.app.config["DEFAULT_WORKFLOW_EVENT_SUBMITTERS"]
-        else:
-            return {}
-
-    def _get_id_from_record(self, record):
-        # community record doesn't have id in dict form, only uuid
-        return record["id"] if "id" in record else record.id
-
-    def get_workflow(self, record):
+    def default_workflow_events(self) -> dict:
+        """Return a dictionary of default workflow events.
+
+        Default workflow events are those that can be added to any request.
+        The dictionary is taken from the configuration key `DEFAULT_WORKFLOW_EVENTS`.
+        """
+        return self.app.config.get("DEFAULT_WORKFLOW_EVENTS", {})
+
+    def get_workflow(self, record: Record) -> Workflow:
+        """Get the workflow for a record.
+
+        :param record:  record to get the workflow for
+        :raises MissingWorkflowError: if the workflow is not found
+        :raises InvalidWorkflowError: if the workflow is invalid
+        """
+        parent = record.parent  # noqa for typing: we do not have a better type for record with parent
         try:
-            return self.record_workflows[record.parent.workflow]
-        except AttributeError:
-            raise MissingWorkflowError(
-                f"Workflow not found on record {self._get_id_from_record(record)}."
-            )
-        except KeyError:
+            return self.record_workflows[parent.workflow]
+        except AttributeError as e:
+            raise MissingWorkflowError("Workflow not found.", record=record) from e
+        except KeyError as e:
             raise InvalidWorkflowError(
-                f"Workflow {record.parent.workflow} on record {self._get_id_from_record(record)} doesn't exist."
-            )
+                f"Workflow {parent.workflow} doesn't exist in the configuration.",
+                record=record,
+            ) from e
 
-    def init_app(self, app) -> None:
+    def init_app(self, app: Flask) -> None:
         """Flask application initialization."""
         self.app = app
         app.extensions["oarepo-workflows"] = self
 
-def finalize_app(app) -> None:
+
+def finalize_app(app: Flask) -> None:
+    """Finalize the application.
+
+    This function registers the auto-approve service in the records resources registry.
+    It is called from invenio_base.api_finalize_app entry point.
+
+    :param app: Flask application
+    """
     records_resources = app.extensions["invenio-records-resources"]
 
     ext = app.extensions["oarepo-workflows"]
@@ -144,4 +242,3 @@ def finalize_app(app) -> None:
         ext.autoapprove_service,
         service_id=ext.autoapprove_service.config.service_id,
     )
-
diff --git a/oarepo_workflows/ext_config.py b/oarepo_workflows/ext_config.py
index 111a928..62b9ba3 100644
--- a/oarepo_workflows/ext_config.py
+++ b/oarepo_workflows/ext_config.py
@@ -1,10 +1,22 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Default configuration for oarepo-workflows."""
+
 from __future__ import annotations
 
-from oarepo_workflows.services.permissions.policy import \
-    WorkflowPermissionPolicy
+from oarepo_workflows.services.permissions.record_permission_policy import (
+    WorkflowRecordPermissionPolicy,
+)
 
 OAREPO_PERMISSIONS_PRESETS = {
-    "workflow": WorkflowPermissionPolicy,
+    "workflow": WorkflowRecordPermissionPolicy,
 }
+"""Permissions presets for oarepo-workflows."""
 
 WORKFLOWS = {}
+"""Configuration of workflows, must be provided by the user inside, for example, invenio.cfg."""
diff --git a/oarepo_workflows/proxies.py b/oarepo_workflows/proxies.py
index 15c86ec..58c803b 100644
--- a/oarepo_workflows/proxies.py
+++ b/oarepo_workflows/proxies.py
@@ -1,3 +1,12 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Proxies for accessing the current OARepo workflows extension without bringing dependencies."""
+
 from __future__ import annotations
 
 from flask import current_app
@@ -6,3 +15,4 @@
 current_oarepo_workflows = LocalProxy(
     lambda: current_app.extensions["oarepo-workflows"]
 )
+"""Proxy to access the current OARepo workflows extension."""
diff --git a/oarepo_workflows/records/__init__.py b/oarepo_workflows/records/__init__.py
index e69de29..57960f4 100644
--- a/oarepo_workflows/records/__init__.py
+++ b/oarepo_workflows/records/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Record layer."""
diff --git a/oarepo_workflows/records/models.py b/oarepo_workflows/records/models.py
index e7da3ec..5c19413 100644
--- a/oarepo_workflows/records/models.py
+++ b/oarepo_workflows/records/models.py
@@ -1,3 +1,12 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Model mixins."""
+
 from __future__ import annotations
 
 from invenio_db import db
@@ -5,4 +14,7 @@
 
 
 class RecordWorkflowParentModelMixin:
+    """Mixin for a ParentRecord database model that adds a workflow field."""
+
     workflow = db.Column(String)
+    """Workflow identifier."""
diff --git a/oarepo_workflows/records/systemfields/__init__.py b/oarepo_workflows/records/systemfields/__init__.py
index e69de29..95d503f 100644
--- a/oarepo_workflows/records/systemfields/__init__.py
+++ b/oarepo_workflows/records/systemfields/__init__.py
@@ -0,0 +1,16 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Record layer, system fields."""
+
+from .state import RecordStateField
+from .workflow import WorkflowField
+
+__all__ = (
+    "RecordStateField",
+    "WorkflowField",
+)
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index 06bd03c..d66071d 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -1,33 +1,70 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""State system field."""
+
 from __future__ import annotations
 
-from typing import Self
+from typing import Any, Protocol, Self, overload
 
 from invenio_records.systemfields.base import SystemField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
 
 
+class WithState(Protocol):
+    """A protocol for a record containing a state field.
+
+    Later on, if typing.Intersection is implemented,
+    one could use it to have the record correctly typed as
+    record: Intersection[WithState, Record]
+    """
+
+    state: str
+    """State of the record."""
+
+
 class RecordStateField(MappingSystemFieldMixin, SystemField):
-    def __init__(self, key: str="state", initial: str="draft", config=None) -> None:
-        self._config = config
+    """State system field."""
+
+    def __init__(self, key: str = "state", initial: str = "draft") -> None:
+        """Initialize the state field."""
         self._initial = initial
         super().__init__(key=key)
 
-    def post_create(self, record) -> None:
+    def post_create(self, record: WithState) -> None:
+        """Set the initial state when record is created."""
         self.set_dictkey(record, self._initial)
 
-    def post_init(self, record, data, model=None, **kwargs) -> None:
+    def post_init(
+        self, record: WithState, data: dict, model: Any = None, **kwargs: Any
+    ) -> None:
+        """Set the initial state when record is created."""
         if not record.state:
             self.set_dictkey(record, self._initial)
 
-    def __get__(self, record, owner=None) -> str | Self:
+    @overload
+    def __get__(self, record: None, owner: type | None = None) -> Self: ...
+
+    @overload
+    def __get__(self, record: WithState, owner: type | None = None) -> str: ...
+
+    def __get__(
+        self, record: WithState | None, owner: type | None = None
+    ) -> str | Self:
         """Get the persistent identifier."""
         if record is None:
             return self
         return self.get_dictkey(record)
 
-    def __set__(self, record, value) -> None:
+    def __set__(self, record: WithState, value: str) -> None:
+        """Directly set the state of the record."""
         self.set_dictkey(record, value)
 
     @property
     def mapping(self) -> dict[str, dict[str, str]]:
+        """Return the opensearch mapping for the state field."""
         return {self.attr_name: {"type": "keyword"}}
diff --git a/oarepo_workflows/records/systemfields/workflow.py b/oarepo_workflows/records/systemfields/workflow.py
index 43a1d2b..88606f8 100644
--- a/oarepo_workflows/records/systemfields/workflow.py
+++ b/oarepo_workflows/records/systemfields/workflow.py
@@ -1,3 +1,12 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Workflow system field."""
+
 from __future__ import annotations
 
 from invenio_records.systemfields.model import ModelField
@@ -5,11 +14,14 @@
 
 
 class WorkflowField(MappingSystemFieldMixin, ModelField):
+    """Workflow system field, should be defined on ParentRecord."""
 
     def __init__(self) -> None:
+        """Initialize the workflow field."""
         self._workflow = None  # added in db
         super().__init__(model_field_name="workflow", key="workflow")
 
     @property
     def mapping(self) -> dict[str, dict[str, str]]:
+        """Elasticsearch mapping."""
         return {self.attr_name: {"type": "keyword"}}
diff --git a/oarepo_workflows/requests/__init__.py b/oarepo_workflows/requests/__init__.py
index d638c5c..296bab3 100644
--- a/oarepo_workflows/requests/__init__.py
+++ b/oarepo_workflows/requests/__init__.py
@@ -1,18 +1,32 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Requests layer."""
+
 from __future__ import annotations
 
-from .policy import (AutoApprove, AutoRequest, RecipientEntityReference,
-                     RecipientGeneratorMixin, RequesterGenerator,
-                     WorkflowRequest, WorkflowRequestEscalation,
-                     WorkflowRequestPolicy, WorkflowTransitions)
+from .generators import AutoApprove, AutoRequest, RecipientGeneratorMixin
+from .policy import (
+    WorkflowRequestPolicy,
+)
+from .requests import (
+    RecipientEntityReference,
+    WorkflowRequest,
+    WorkflowRequestEscalation,
+    WorkflowTransitions,
+)
 
 __all__ = (
     "WorkflowRequestPolicy",
-    "WorkflowRequest",
-    "WorkflowTransitions",
-    "RecipientGeneratorMixin",
-    "WorkflowRequestEscalation",
     "AutoApprove",
     "AutoRequest",
-    "RequesterGenerator",
-    "RecipientEntityReference"
+    "RecipientGeneratorMixin",
+    "RecipientEntityReference",
+    "WorkflowRequest",
+    "WorkflowRequestEscalation",
+    "WorkflowTransitions",
 )
diff --git a/oarepo_workflows/requests/events.py b/oarepo_workflows/requests/events.py
index a6188c2..f80e0fb 100644
--- a/oarepo_workflows/requests/events.py
+++ b/oarepo_workflows/requests/events.py
@@ -1,31 +1,36 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Events for workflow requests."""
+
 from __future__ import annotations
 
 import dataclasses
-from typing import List, Tuple
+from functools import cached_property
+from typing import TYPE_CHECKING
+
+from oarepo_workflows.requests import MultipleGeneratorsGenerator
 
-from flask_principal import Need
-from invenio_records_permissions.generators import Generator
+if TYPE_CHECKING:
+    from invenio_records_permissions.generators import Generator
 
 
 @dataclasses.dataclass
 class WorkflowEvent:
-    submitters: List[Generator] | Tuple[Generator]
-
-    def needs(self, **kwargs) -> set[Need]:
-        return {
-            need for generator in self.submitters for need in generator.needs(**kwargs)
-        }
-
-    def excludes(self, **kwargs) -> set[Need]:
-        return {
-            exclude
-            for generator in self.submitters
-            for exclude in generator.excludes(**kwargs)
-        }
-
-    def query_filters(self, **kwargs) -> list[dict]:
-        return [
-            query_filter
-            for generator in self.submitters
-            for query_filter in generator.query_filter(**kwargs)
-        ]
+    """Class representing a workflow event."""
+
+    submitters: list[Generator] | tuple[Generator]
+    """List of submitters to be used for the event.
+
+       The generators supply needs. The user must have at least one of the needs 
+       to be able to create a workflow event.
+    """
+
+    @cached_property
+    def submitter_generator(self) -> Generator:
+        """Return the requesters as a single requester generator."""
+        return MultipleGeneratorsGenerator(self.submitters)
diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators.py
new file mode 100644
index 0000000..4510504
--- /dev/null
+++ b/oarepo_workflows/requests/generators.py
@@ -0,0 +1,116 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Need generators."""
+
+from __future__ import annotations
+
+import dataclasses
+from typing import TYPE_CHECKING, Any
+
+from invenio_access import SystemRoleNeed
+from invenio_records_permissions.generators import Generator
+
+if TYPE_CHECKING:
+    from flask_principal import Need
+    from invenio_records_resources.records import Record
+    from invenio_requests.customizations.request_types import RequestType
+
+
+@dataclasses.dataclass
+class MultipleGeneratorsGenerator(Generator):
+    """A generator that combines multiple generators with 'or' operation."""
+
+    generators: list[Generator] | tuple[Generator]
+    """List of generators to be combined."""
+
+    def needs(self, **context: Any) -> set[Need]:
+        """Generate a set of needs from generators that a person needs to have.
+
+        :param context: Context.
+        :return: Set of needs.
+        """
+        return {
+            need for generator in self.generators for need in generator.needs(**context)
+        }
+
+    def excludes(self, **context: Any) -> set[Need]:
+        """Generate a set of needs that person must not have.
+
+        :param context: Context.
+        :return: Set of needs.
+        """
+        return {
+            exclude
+            for generator in self.generators
+            for exclude in generator.excludes(**context)
+        }
+
+    def query_filters(self, **context: Any) -> list[dict]:
+        """Generate a list of opensearch query filters.
+
+         These filters are used to filter objects. These objects are governed by a policy
+         containing this generator.
+
+        :param context: Context.
+        """
+        return [
+            query_filter
+            for generator in self.generators
+            for query_filter in generator.query_filter(**context)
+        ]
+
+
+auto_request_need = SystemRoleNeed("auto_request")
+auto_approve_need = SystemRoleNeed("auto_approve")
+
+
+class AutoRequest(Generator):
+    """Auto request generator.
+
+    This generator is used to automatically create a request
+    when a record is moved to a specific state.
+    """
+
+    def needs(self, **context: Any) -> list[Need]:
+        """Get needs that signal workflow to automatically create the request."""
+        return [auto_request_need]
+
+
+class RecipientGeneratorMixin:
+    """Mixin for permission generators that can be used as recipients in WorkflowRequest."""
+
+    def reference_receivers(
+        self, record: Record = None, request_type: RequestType = None, **context: Any
+    ) -> list[dict[str, str]]:
+        """Return the reference receiver(s) of the request.
+
+        This call requires the context to contain at least "record" and "request_type"
+
+        Must return a list of dictionary serialization of the receivers.
+
+        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 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: Record = None, request_type: RequestType = None, **kwargs: Any
+    ) -> list[dict[str, str]]:
+        """Return the reference receiver(s) of the auto-approve request.
+
+        Returning "auto_approve" is a signal to the workflow that the request should be auto-approved.
+        """
+        return [{"auto_approve": "true"}]
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index b83c02c..b628a6c 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -1,131 +1,35 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows 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 workflow policy definition."""
+
 from __future__ import annotations
 
-import dataclasses
-from datetime import timedelta
 from functools import cached_property
-from logging import getLogger
-from typing import Dict, List, Optional, Tuple
-
-from flask_principal import Identity, Need, Permission
-from invenio_access.permissions import SystemRoleNeed
-from invenio_records_permissions.generators import Generator
-
-from oarepo_workflows.proxies import current_oarepo_workflows
-from oarepo_workflows.requests.events import WorkflowEvent
-
-log = getLogger(__name__)
-
-
-@dataclasses.dataclass
-class WorkflowRequest:
-    """
-    Workflow request definition. The request is defined by the requesters and recipients.
-    The requesters are the generators that define who can submit the request. The recipients
-    are the generators that define who can approve the request.
-    """
-
-    requesters: List[Generator] | Tuple[Generator]
-    """Generators that define who can submit the request."""
-
-    recipients: List[Generator] | Tuple[Generator]
-    """Generators that define who can approve the request."""
-
-    events: Dict[str, WorkflowEvent] = dataclasses.field(default_factory=lambda: {})
-    """Events that can be submitted with the request."""
-
-    transitions: Optional[WorkflowTransitions] = dataclasses.field(
-        default_factory=lambda: WorkflowTransitions()
-    )
-    """Transitions applied to the state of the topic of the request."""
-
-    escalations: Optional[List[WorkflowRequestEscalation]] = None
-    """Escalations applied to the request if not approved/declined in time."""
+from typing import TYPE_CHECKING, Any
 
-    @cached_property
-    def requester_generator(self) -> Generator:
-        """
-        Return the requesters as a single requester generator.
-        """
-        return RequesterGenerator(self.requesters)
+from .requests import (
+    WorkflowRequest,
+)
 
-    def recipient_entity_reference(self, **context) -> Dict | None:
-        """
-        Return the reference receiver of the workflow request with the given context.
-        Note: invenio supports only one receiver, so the first one is returned at the moment.
-        Later on, a composite receiver can be implemented.
-
-        :param context: Context of the request.
-        """
-        return RecipientEntityReference(self, **context)
-
-    def is_applicable(self, identity: Identity, **context) -> bool:
-        """
-        Check if the request is applicable for the identity and context (which might include record, community, ...).
-
-        :param identity: Identity of the requester.
-        :param context: Context of the request that is passed to the requester generators.
-        """
-        try:
-            p = Permission(*self.requester_generator.needs(**context))
-            if not p.needs:
-                return False
-            p.excludes.update(self.requester_generator.excludes(**context))
-            return p.allows(identity)
-        except Exception as e:
-            log.exception("Error checking request applicability: %s", e)
-            return False
-
-    @property
-    def allowed_events(self) -> Dict[str, WorkflowEvent]:
-        """
-        Return the allowed events for the workflow request.
-        """
-        return current_oarepo_workflows.default_workflow_event_submitters | self.events
-
-
-@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
-    accepted: Optional[str] = None
-    declined: Optional[str] = None
-
-    def __getitem__(self, item):
-        try:
-            return getattr(self, item)
-        except AttributeError:
-            raise KeyError(
-                f"Transition {item} not defined in {self.__class__.__name__}"
-            )
-
-
-@dataclasses.dataclass
-class WorkflowRequestEscalation:
-    """
-    If the request is not approved/declined/cancelled in time, it might be passed to another recipient
-    (such as a supervisor, administrator, ...). The escalation is defined by the time after which the
-    request is escalated and the recipients of the escalation.
-    """
-
-    after: timedelta
-    recipients: List[Generator] | Tuple[Generator]
+if TYPE_CHECKING:
+    from flask_principal import Identity
 
 
 class WorkflowRequestPolicy:
-    """Base class for workflow request policies. Inherit from this class
+    """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 = [
@@ -138,18 +42,24 @@ class MyWorkflowRequests(WorkflowRequestPolicy):
                     rejected = 'published'
                 )
             )
+
     """
 
-    def __getitem__(self, item):
+    def __getitem__(self, request_type_id: str) -> WorkflowRequest:
+        """Get the workflow request type by its id."""
         try:
-            return getattr(self, item)
+            return getattr(self, request_type_id)
         except AttributeError:
             raise KeyError(
-                f"Request type {item} not defined in {self.__class__.__name__}"
-            )
+                f"Request type {request_type_id} not defined in {self.__class__.__name__}"
+            ) from None
 
     @cached_property
     def items(self) -> list[tuple[str, WorkflowRequest]]:
+        """Return the list of request types and their instances.
+
+        This call mimics mapping items() method.
+        """
         ret = []
         parent_attrs = set(dir(WorkflowRequestPolicy))
         for attr in dir(self.__class__):
@@ -162,9 +72,14 @@ def items(self) -> list[tuple[str, WorkflowRequest]]:
                 ret.append((attr, possible_request))
         return ret
 
-    def applicable_workflow_requests(self, identity, **context) -> list[tuple[str, WorkflowRequest]]:
-        """
-        Return a list of applicable requests for the identity and context.
+    def applicable_workflow_requests(
+        self, identity: Identity, **context: Any
+    ) -> list[tuple[str, WorkflowRequest]]:
+        """Return a list of applicable requests for the identity and context.
+
+        :param identity: Identity of the requester.
+        :param context: Context of the request that is passed to the requester generators.
+        :return List of tuples (request_type_id, request) that are applicable for the identity and context.
         """
         ret = []
 
@@ -172,124 +87,3 @@ def applicable_workflow_requests(self, identity, **context) -> list[tuple[str, W
             if request.is_applicable(identity, **context):
                 ret.append((name, request))
         return ret
-
-
-auto_request_need = SystemRoleNeed("auto_request")
-auto_approve_need = SystemRoleNeed("auto_approve")
-
-
-class AutoRequest(Generator):
-    """
-    Auto request generator. This generator is used to automatically create a request
-    when a record is moved to a specific state.
-    """
-
-    def needs(self, **kwargs) -> list[Need]:
-        """Enabling Needs."""
-        return [auto_request_need]
-
-
-class RecipientGeneratorMixin:
-    """
-    Mixin for permission generators that can be used as recipients in WorkflowRequest.
-    """
-
-    def reference_receivers(self, record=None, request_type=None, **kwargs) -> List[Dict]:
-        """
-        Taken the context (will include record amd request type at least),
-        return the reference receiver(s) of the request.
-
-        Should return a list of 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 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) -> list[dict[str, str]]:
-        return [{"auto_approve": "true"}]
-
-
-class RequesterGenerator(Generator):
-    def __init__(self, requesters) -> None:
-        super().__init__()
-        self.requesters = requesters
-
-    def needs(self, **context) -> set[Need]:
-        """
-        Generate a set of needs that requester needs to have in order to create the request.
-
-        :param context: Context of the request.
-        :return: Set of needs.
-        """
-
-        return {
-            need for generator in self.requesters for need in generator.needs(**context)
-        }
-
-    def excludes(self, **context) -> set[Need]:
-        """
-        Generate a set of needs that requester must not have in order to create the request.
-
-        :param context: Context of the request.
-        :return: Set of needs.
-        """
-        return {
-            exclude
-            for generator in self.requesters
-            for exclude in generator.excludes(**context)
-        }
-
-    def query_filters(self, **context) -> list[dict]:
-        """
-        Generate a list of opensearch query filters to get the listing of matching requests.
-
-        :param context: Context of the request.
-        """
-
-        return [
-            query_filter
-            for generator in self.requesters
-            for query_filter in generator.query_filter(**context)
-        ]
-
-
-def RecipientEntityReference(
-    request: WorkflowRequest, **context
-) -> Dict | None:
-    """
-    Return the reference receiver of the workflow request with the given context.
-    Note: invenio supports only one receiver, so the first one is returned at the moment.
-    Later on, a composite receiver can be implemented.
-
-    :param request: Workflow request.
-    :param context: Context of the request.
-
-    :return: Reference receiver as a dictionary or None if no receiver has been resolved.
-
-    Implementation note: intentionally looks like a class, later on might be converted into one extending from dict.
-    """
-
-    if not request.recipients:
-        return None
-
-    all_receivers = []
-    for generator in request.recipients:
-        if isinstance(generator, RecipientGeneratorMixin):
-            ref: list[dict] = generator.reference_receivers(**context)
-            if ref:
-                all_receivers.extend(ref)
-
-    if all_receivers:
-        if len(all_receivers) > 1:
-            log.debug("Multiple receivers for request %s: %s", request, all_receivers)
-        return all_receivers[0]
-
-    return None
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
new file mode 100644
index 0000000..c5bdbd7
--- /dev/null
+++ b/oarepo_workflows/requests/requests.py
@@ -0,0 +1,161 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Workflow requests."""
+
+from __future__ import annotations
+
+import dataclasses
+from functools import cached_property
+from logging import getLogger
+from typing import TYPE_CHECKING, Any, Optional
+
+from flask_principal import Identity, Permission
+
+from oarepo_workflows.proxies import current_oarepo_workflows
+from oarepo_workflows.requests import RecipientGeneratorMixin
+from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
+
+if TYPE_CHECKING:
+    from datetime import timedelta
+
+    from invenio_records_permissions.generators import Generator
+
+    from oarepo_workflows.requests.events import WorkflowEvent
+
+log = getLogger(__name__)
+
+
+@dataclasses.dataclass
+class WorkflowRequest:
+    """Workflow request definition.
+
+    The request is defined by the requesters and recipients.
+    The requesters are the generators that define who can submit the request. The recipients
+    are the generators that define who can approve the request.
+    """
+
+    requesters: list[Generator] | tuple[Generator]
+    """Generators that define who can submit the request."""
+
+    recipients: list[Generator] | tuple[Generator]
+    """Generators that define who can approve the request."""
+
+    events: dict[str, WorkflowEvent] = dataclasses.field(default_factory=lambda: {})
+    """Events that can be submitted with the request."""
+
+    transitions: Optional[WorkflowTransitions] = dataclasses.field(
+        default_factory=lambda: WorkflowTransitions()
+    )
+    """Transitions applied to the state of the topic of the request."""
+
+    escalations: Optional[list[WorkflowRequestEscalation]] = None
+    """Escalations applied to the request if not approved/declined in time."""
+
+    @cached_property
+    def requester_generator(self) -> Generator:
+        """Return the requesters as a single requester generator."""
+        return MultipleGeneratorsGenerator(self.requesters)
+
+    def recipient_entity_reference(self, **context: Any) -> dict | None:
+        """Return the reference receiver of the workflow request with the given context.
+
+        Note: invenio supports only one receiver, so the first one is returned at the moment.
+        Later on, a composite receiver can be implemented.
+
+        :param context: Context of the request.
+        """
+        return RecipientEntityReference(self, **context)
+
+    def is_applicable(self, identity: Identity, **context: Any) -> bool:
+        """Check if the request is applicable for the identity and context (which might include record, community, ...).
+
+        :param identity: Identity of the requester.
+        :param context: Context of the request that is passed to the requester generators.
+        """
+        try:
+            p = Permission(*self.requester_generator.needs(**context))
+            if not p.needs:
+                return False
+            p.excludes.update(self.requester_generator.excludes(**context))
+            return p.allows(identity)
+        except Exception as e:
+            log.exception("Error checking request applicability: %s", e)
+            return False
+
+    @property
+    def allowed_events(self) -> dict[str, WorkflowEvent]:
+        """Return the allowed events for the workflow request."""
+        return current_oarepo_workflows.default_workflow_events | self.events
+
+
+@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
+    accepted: Optional[str] = None
+    declined: Optional[str] = None
+
+    def __getitem__(self, transition_name: str):
+        """Get the transition by name."""
+        try:
+            return getattr(self, transition_name)
+        except AttributeError:
+            raise KeyError(
+                f"Transition {transition_name} not defined in {self.__class__.__name__}"
+            ) from None
+
+
+@dataclasses.dataclass
+class WorkflowRequestEscalation:
+    """Escalation of the request.
+
+    If the request is not approved/declined/cancelled in time, it might be passed to another recipient
+    (such as a supervisor, administrator, ...). The escalation is defined by the time after which the
+    request is escalated and the recipients of the escalation.
+    """
+
+    after: timedelta
+    recipients: list[Generator] | tuple[Generator]
+
+
+def RecipientEntityReference(request: WorkflowRequest, **context: Any) -> dict | None:
+    """Return the reference receiver of the workflow request with the given context.
+
+    Note: invenio supports only one receiver, so the first one is returned at the moment.
+    Later on, a composite receiver can be implemented.
+
+    :param request: Workflow request.
+    :param context: Context of the request.
+
+    :return: Reference receiver as a dictionary or None if no receiver has been resolved.
+
+    Implementation note: intentionally looks like a class, later on might be converted into one extending from dict.
+    """
+    if not request.recipients:
+        return None
+
+    all_receivers = []
+    for generator in request.recipients:
+        if isinstance(generator, RecipientGeneratorMixin):
+            ref: list[dict] = generator.reference_receivers(**context)
+            if ref:
+                all_receivers.extend(ref)
+
+    if all_receivers:
+        if len(all_receivers) > 1:
+            log.debug("Multiple receivers for request %s: %s", request, all_receivers)
+        return all_receivers[0]
+
+    return None
diff --git a/oarepo_workflows/resolvers/__init__.py b/oarepo_workflows/resolvers/__init__.py
index e69de29..10071d3 100644
--- a/oarepo_workflows/resolvers/__init__.py
+++ b/oarepo_workflows/resolvers/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Entity resolvers."""
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index cd5950c..683b8f9 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -1,45 +1,68 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Auto approve entity and resolver."""
+
 from __future__ import annotations
 
-from flask_principal import Need
+from typing import TYPE_CHECKING, Any
+
 from invenio_records_resources.references.entity_resolvers import EntityProxy
-from invenio_records_resources.references.entity_resolvers.base import \
-    EntityResolver
+from invenio_records_resources.references.entity_resolvers.base import EntityResolver
+
+if TYPE_CHECKING:
+    from flask_principal import Identity, Need
 
 
-class AutoApprover:
-    def __init__(self, value: bool) -> None:
-        self.value = value
+class AutoApproveEntity:
+    """Entity representing auto approve."""
 
 
 class AutoApproveProxy(EntityProxy):
-    def _resolve(self) -> AutoApprover:
-        value = self._parse_ref_dict_id()
-        return AutoApprover(value)
+    """Proxy for auto approve entity."""
 
-    def get_needs(self, ctx=None) -> list[Need]:
+    def _resolve(self) -> AutoApproveEntity:
+        """Resolve the entity reference into entity."""
+        return AutoApproveEntity()
+
+    def get_needs(self, ctx: dict | None = None) -> list[Need]:
+        """Get needs that the entity generate."""
         return []  # granttokens calls this
 
-    def pick_resolved_fields(self, identity, resolved_dict) -> dict:
+    def pick_resolved_fields(self, identity: Identity, resolved_dict: dict) -> dict:
+        """Pick resolved fields for serialization of the entity to json."""
         return {"auto_approve": resolved_dict["id"]}
 
 
 class AutoApproveResolver(EntityResolver):
+    """A resolver that resolves auto approve entity."""
+
     type_id = "auto_approve"
 
     def __init__(self) -> None:
-        self.type_key = self.type_id
-        super().__init__(
-            "auto_approve",
-        )
+        """Initialize the resolver."""
+        super().__init__("auto_approve")
 
-    def matches_reference_dict(self, ref_dict) -> bool:
+    def matches_reference_dict(self, ref_dict: dict) -> bool:
+        """Check if the reference dictionary can be resolved by this resolver."""
         return self._parse_ref_dict_type(ref_dict) == self.type_id
 
-    def _reference_entity(self, entity) -> dict[str, str]:
-        return {self.type_key: str(entity.value)}
+    def _reference_entity(self, entity: Any) -> dict[str, str]:
+        """Return a reference dictionary for the entity."""
+        return {self.type_id: str(True)}
+
+    def matches_entity(self, entity: Any) -> bool:
+        """Check if the entity can be serialized to a reference by this resolver."""
+        return isinstance(entity, AutoApproveEntity)
 
-    def matches_entity(self, entity) -> bool:
-        return isinstance(entity, AutoApprover)
+    def _get_entity_proxy(self, ref_dict: dict) -> AutoApproveProxy:
+        """Get the entity proxy for the reference dictionary.
 
-    def _get_entity_proxy(self, ref_dict) -> AutoApproveProxy:
+        Note: the proxy is returned to ensure the entity is loaded lazily, when needed.
+        :param ref_dict: Reference dictionary.
+        """
         return AutoApproveProxy(self, ref_dict)
diff --git a/oarepo_workflows/services/__init__.py b/oarepo_workflows/services/__init__.py
index e69de29..038ae23 100644
--- a/oarepo_workflows/services/__init__.py
+++ b/oarepo_workflows/services/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Service layer."""
diff --git a/oarepo_workflows/services/auto_approve/__init__.py b/oarepo_workflows/services/auto_approve/__init__.py
index 720e698..e0a8132 100644
--- a/oarepo_workflows/services/auto_approve/__init__.py
+++ b/oarepo_workflows/services/auto_approve/__init__.py
@@ -1,3 +1,16 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Service for reading auto-approve entities.
+
+The implementation is simple as auto approve is just one entity
+so there is no need to store it to database/fetch it from the database.
+"""
+
 from __future__ import annotations
 
 from oarepo_runtime.services.entity.config import KeywordEntityServiceConfig
@@ -5,9 +18,13 @@
 
 
 class AutoApproveEntityServiceConfig(KeywordEntityServiceConfig):
+    """Configuration for auto-approve entity service."""
+
     service_id = "auto_approve"
     keyword = "auto_approve"
 
 
 class AutoApproveEntityService(KeywordEntityService):
+    """Service for reading auto-approve entities."""
+
     pass
diff --git a/oarepo_workflows/services/components/__init__.py b/oarepo_workflows/services/components/__init__.py
index e69de29..c0150e2 100644
--- a/oarepo_workflows/services/components/__init__.py
+++ b/oarepo_workflows/services/components/__init__.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Service components for supporting workflows on Invenio records."""
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 1829014..63669e9 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -1,18 +1,48 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Service components for supporting workflows on Invenio records."""
+
 from __future__ import annotations
 
-from invenio_records_resources.services.records.components.base import \
-    ServiceComponent
+from typing import TYPE_CHECKING, Any
+
+from invenio_records_resources.services.records.components.base import ServiceComponent
 
 from oarepo_workflows.errors import MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
 
+if TYPE_CHECKING:
+    from flask_principal import Identity
+    from invenio_records_resources.records import Record
+
 
 class WorkflowComponent(ServiceComponent):
-    def create(self, identity, data=None, record=None, **kwargs) -> None:
+    """Workflow component.
+
+    This component is responsible for checking if the workflow is defined in the input data
+    when record is created. If it is not present, it raises an error.
+    """
+
+    def create(
+        self,
+        identity: Identity,
+        data: dict = None,
+        record: Record = None,
+        **kwargs: Any,
+    ) -> None:
+        """Implement record creation checks and set the workflow on the created record."""
         try:
             workflow_id = data["parent"]["workflow"]
-        except KeyError:
-            raise MissingWorkflowError("Workflow not defined in input.")
+        except KeyError as e:
+            raise MissingWorkflowError(
+                "Workflow not defined in input.", record=data
+            ) from e
+
         current_oarepo_workflows.set_workflow(
             identity, record, workflow_id, uow=self.uow, **kwargs
         )
diff --git a/oarepo_workflows/services/permissions/__init__.py b/oarepo_workflows/services/permissions/__init__.py
index 8fb5c14..efb3e97 100644
--- a/oarepo_workflows/services/permissions/__init__.py
+++ b/oarepo_workflows/services/permissions/__init__.py
@@ -1,13 +1,21 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Permissions for workflows."""
+
 from __future__ import annotations
 
-from .generators import AutoApprove, AutoRequest, IfInState, WorkflowPermission
-from .policy import DefaultWorkflowPermissions, WorkflowPermissionPolicy
+from .generators import IfInState, WorkflowPermission
+from .record_permission_policy import WorkflowRecordPermissionPolicy
+from .workflow_permissions import DefaultWorkflowPermissions
 
 __all__ = (
     "IfInState",
     "WorkflowPermission",
+    "WorkflowRecordPermissionPolicy",
     "DefaultWorkflowPermissions",
-    "WorkflowPermissionPolicy",
-    "AutoApprove",
-    "AutoRequest",
 )
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index a36016b..6895560 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -1,128 +1,201 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Permission generators usable in workflow configurations."""
+
 from __future__ import annotations
 
 import operator
 from functools import reduce
 from itertools import chain
-from typing import Optional
+from typing import TYPE_CHECKING, Any, Iterable
 
-from flask import Need
-from invenio_records_permissions.generators import (ConditionalGenerator,
-                                                    Generator)
+from invenio_records_permissions.generators import ConditionalGenerator, Generator
 from invenio_search.engine import dsl
 
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
 from oarepo_workflows.proxies import current_oarepo_workflows
-from oarepo_workflows.requests.policy import RecipientGeneratorMixin
-from oarepo_workflows.services.permissions.identity import auto_request_need
+
+if TYPE_CHECKING:
+    from flask_principal import Need
+    from invenio_records_permissions import RecordPermissionPolicy
+    from invenio_records_resources.records import Record
 
 
 # invenio_records_permissions.generators.ConditionalGenerator._make_query
-def _make_query(generators, **kwargs) -> dict:
-    queries = [g.query_filter(**kwargs) for g in generators]
+def _make_query(generators: Iterable[Generator], **context: Any) -> dict:
+    queries = [g.query_filter(**context) for g in generators]
     queries = [q for q in queries if q]
     return reduce(operator.or_, queries) if queries else None
 
 
 class WorkflowPermission(Generator):
-    def __init__(self, action=None) -> None:
+    """Permission delegating check to workflow.
+
+    The implementation of the permission gets the workflow id from the passed context
+    (record or data) and then looks up the workflow definition in the configuration.
+
+    The workflow definition must contain a permissions policy that is then used to
+    determine the permissions for the action.
+    """
+
+    def __init__(self, action: str | None = None) -> None:
+        """Initialize the permission."""
         # might not be needed in subclasses
         super().__init__()
         self._action = action
 
-    def _get_workflow_id(self, record=None, **kwargs):
+    def _get_workflow_id(self, record: Record = None, **context: Any) -> str:
+        """Get the workflow id from the context.
+
+        If the record is passed, the workflow is determined from the record.
+        If the record is not passed, the workflow is determined from the input data.
+
+        If the workflow is not found, an error is raised.
+
+        :param record: Record to get the workflow from.
+        :param context: Context to get the workflow from.
+        :return: Workflow id.
+        :raises MissingWorkflowError: If the workflow is not found on the record/data.
+        """
         if record:
             workflow_id = current_oarepo_workflows.get_workflow_from_record(record)
             if not workflow_id:
-                raise MissingWorkflowError("Workflow not defined on record.")
+                raise MissingWorkflowError(
+                    "Workflow not defined on record.", record=record
+                )
         else:
-            workflow_id = kwargs.get("data", {}).get("parent", {}).get("workflow", {})
+            data = context.get("data", {})
+            workflow_id = data.get("parent", {}).get("workflow", {})
             if not workflow_id:
-                raise MissingWorkflowError("Workflow not defined in input.")
+                raise MissingWorkflowError(
+                    "Workflow not defined in input.", record=data
+                )
         return workflow_id
 
-    def _get_permissions_from_workflow(self, record=None, action_name: Optional[str]=None, **kwargs):
-        workflow_id = self._get_workflow_id(record, **kwargs)
+    def _get_permissions_from_workflow(
+        self, record: Record = None, action_name: str | None = None, **context: Any
+    ) -> RecordPermissionPolicy:
+        """Get the permissions policy from the workflow.
+
+        At first the workflow id is determined from the context.
+        Then the permissions policy is determined from the workflow configuration,
+        is instantiated with the action name and the context and the permissions
+        for the action are returned.
+        """
+        workflow_id = self._get_workflow_id(record, **context)
         if workflow_id not in current_oarepo_workflows.record_workflows:
             raise InvalidWorkflowError(
-                f"Workflow {workflow_id} does not exist in the configuration."
+                f"Workflow {workflow_id} does not exist in the configuration.",
+                record=record or context.get("data", {}),
             )
         policy = current_oarepo_workflows.record_workflows[workflow_id].permissions
-        return policy(action_name, record=record, **kwargs)
+        return policy(action_name, record=record, **context)
 
-    def needs(self, record=None, **kwargs):
-        return self._get_permissions_from_workflow(record, self._action, **kwargs).needs
+    def needs(self, record: Record = None, **context: Any) -> set[Need]:
+        """Return needs that are generated by the workflow permission."""
+        return self._get_permissions_from_workflow(
+            record, self._action, **context
+        ).needs
 
-    def query_filter(self, record=None, **kwargs):
+    def query_filter(self, record: Record = None, **context: Any) -> dict:
+        """Return query filters that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(
-            record, self._action, **kwargs
+            record, self._action, **context
         ).query_filters
 
 
 class IfInState(ConditionalGenerator):
-    def __init__(self, state, then_) -> None:
-        super().__init__(then_, else_=[])
+    """Generator that checks if the record is in a specific state.
+
+    If it is in the state, the then_ generators are used, otherwise the else_ generators are used.
+
+    Example:
+        .. code-block:: python
+
+            can_edit = [ IfInState("draft", [RecordOwners()]) ]
+
+    """
+
+    def __init__(
+        self,
+        state: str,
+        then_: list[Generator] | tuple[Generator] | None = None,
+        else_: list[Generator] | tuple[Generator] | None = None,
+    ) -> None:
+        """Initialize the generator."""
+        super().__init__(then_ or [], else_=else_ or [])
         self.state = state
 
-    def _condition(self, record, **kwargs) -> bool:
+    def _condition(self, record: Record, **context: Any) -> bool:
+        """Check if the record is in the state."""
         try:
-            state = record.state
+            return record.state == self.state  # noqa as AttributeError is caught
         except AttributeError:
             return False
-        return state == self.state
 
-    def query_filter(self, **kwargs):
-        """Filters for queries."""
+    def query_filter(self, **context: Any) -> dsl.Q:
+        """Apply then or else filter."""
         field = "state"
 
         q_instate = dsl.Q("term", **{field: self.state})
-        then_query = self._make_query(self.then_, **kwargs)
+        if self.then_:
+            then_query = self._make_query(self.then_, **context)
+        else:
+            then_query = dsl.Q("match_none")
 
-        return q_instate & then_query
+        if self.else_:
+            else_query = self._make_query(self.else_, **context)
+        else:
+            else_query = dsl.Q("match_none")
+
+        return (q_instate & then_query) | (~q_instate & else_query)
 
 
 class SameAs(Generator):
-    def __init__(self, as_) -> None:
-        self.as_ = as_
+    """Generator that delegates the permissions to another action.
+
+    Example:
+        .. code-block:: python
+            class Perms:
+                can_create_files = [SameAs("edit_files")]
+                can_edit_files = [RecordOwners()]
 
-    def _generators(self, policy, **kwargs):
-        return getattr(policy, self.as_)
+    would mean that the permissions for creating files are the same as for editing files.
+    This works even if you inherit from the class and override the can_edit_files.
+
+    """
 
-    def needs(self, policy, **kwargs) -> set[Need]:
+    def __init__(self, action: str) -> None:
+        """Initialize the generator."""
+        self.action_generator_name = f"can_{action}"
+
+    def _generators(
+        self, policy: RecordPermissionPolicy, **context: Any
+    ) -> list[Generator]:
+        """Get the generators from the policy."""
+        return getattr(policy, self.action_generator_name)
+
+    def needs(self, policy: RecordPermissionPolicy, **context: Any) -> set[Need]:
+        """Get the needs from the policy."""
         needs = [
-            generator.needs(**kwargs)
-            for generator in self._generators(policy, **kwargs)
+            generator.needs(**context)
+            for generator in self._generators(policy, **context)
         ]
         return set(chain.from_iterable(needs))
 
-    def excludes(self, policy, **kwargs) -> set[Need]:
-        """Preventing Needs."""
+    def excludes(self, policy: RecordPermissionPolicy, **context: Any) -> set[Need]:
+        """Get the excludes from the policy."""
         excludes = [
-            generator.excludes(**kwargs)
-            for generator in self._generators(policy, **kwargs)
+            generator.excludes(**context)
+            for generator in self._generators(policy, **context)
         ]
         return set(chain.from_iterable(excludes))
 
-    def query_filter(self, policy, **kwargs) -> dict:
+    def query_filter(self, policy: RecordPermissionPolicy, **context: Any) -> dict:
         """Search filters."""
-        return _make_query(self._generators(policy, **kwargs), **kwargs)
-
-
-class AutoRequest(Generator):
-    """
-    Auto request generator. This generator is used to automatically create a request
-    when a record is moved to a specific state.
-    """
-
-    def needs(self, **kwargs) -> list[Need]:
-        """Enabling Needs."""
-        return [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) -> list[dict[str, str]]:
-        return [{"auto_approve": "true"}]
+        return _make_query(self._generators(policy, **context), **context)
diff --git a/oarepo_workflows/services/permissions/identity.py b/oarepo_workflows/services/permissions/identity.py
deleted file mode 100644
index 89da89b..0000000
--- a/oarepo_workflows/services/permissions/identity.py
+++ /dev/null
@@ -1,5 +0,0 @@
-from __future__ import annotations
-
-from invenio_access.permissions import SystemRoleNeed
-
-auto_request_need = SystemRoleNeed("auto_request")
diff --git a/oarepo_workflows/services/permissions/policy.py b/oarepo_workflows/services/permissions/policy.py
deleted file mode 100644
index 37dc2a3..0000000
--- a/oarepo_workflows/services/permissions/policy.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from __future__ import annotations
-
-from functools import reduce
-from typing import Optional
-
-from invenio_records_permissions import RecordPermissionPolicy
-from invenio_records_permissions.generators import (AnyUser, AuthenticatedUser,
-                                                    Disable, SystemProcess)
-from invenio_search.engine import dsl
-from oarepo_runtime.services.generators import RecordOwners
-
-from ...proxies import current_oarepo_workflows
-from .generators import IfInState, SameAs, WorkflowPermission
-
-
-class DefaultWorkflowPermissions(RecordPermissionPolicy):
-    """
-    Base class for workflow permissions, subclass from it and put the result to Workflow constructor.
-    Example:
-        class MyWorkflowPermissions(DefaultWorkflowPermissions):
-            can_read = [AnyUser()]
-    in invenio.cfg
-    WORKFLOWS = {
-        'default': Workflow(
-            permission_policy_cls = MyWorkflowPermissions, ...
-        )
-    }
-    """
-
-    # new version - update; edit current version - disable -> idk if there's other way than something like IfNoEditDraft/IfNoNewVersionDraft generators-
-
-    files_edit = [
-        IfInState("draft", [RecordOwners()]),
-        IfInState("published", [Disable()]),
-    ]
-
-    system_process = SystemProcess()
-
-    def __init__(self, action_name: Optional[str]=None, **over) -> None:
-        can = getattr(self, f"can_{action_name}")
-        if self.system_process not in can:
-            can.append(self.system_process)
-        over["policy"] = self
-        super().__init__(action_name, **over)
-
-    can_read = [
-        IfInState("draft", [RecordOwners()]),
-        IfInState("published", [AuthenticatedUser()]),
-    ]
-    can_update = [IfInState("draft", [RecordOwners()])]
-    can_delete = [
-        IfInState("draft", [RecordOwners()]),
-    ]
-    can_create = [AuthenticatedUser()]
-    can_publish = [AuthenticatedUser()]
-    can_new_version = [AuthenticatedUser()]
-
-    can_create_files = [SameAs("files_edit")]
-    can_set_content_files = [SameAs("files_edit")]
-    can_commit_files = [SameAs("files_edit")]
-    can_update_files = [SameAs("files_edit")]
-    can_delete_files = [SameAs("files_edit")]
-    can_draft_create_files = [SameAs("files_edit")]
-    can_read_files = [SameAs("can_read")]
-    can_get_content_files = [SameAs("can_read")]
-
-    can_read_draft = [SameAs("can_read")]
-    can_update_draft = [SameAs("can_update")]
-    can_delete_draft = [SameAs("can_delete")]
-
-
-class WorkflowPermissionPolicy(RecordPermissionPolicy):
-    """
-    Permission policy to be used in permission presets directly on RecordServiceConfig.permission_policy_cls
-    Do not use this class in Workflow constructor.
-    """
-
-    can_create = [WorkflowPermission("create")]
-    can_publish = [WorkflowPermission("publish")]
-    can_read = [WorkflowPermission("read")]
-    can_update = [WorkflowPermission("update")]
-    can_delete = [WorkflowPermission("delete")]
-    can_create_files = [WorkflowPermission("create_files")]
-    can_set_content_files = [WorkflowPermission("set_content_files")]
-    can_get_content_files = [WorkflowPermission("get_content_files")]
-    can_commit_files = [WorkflowPermission("commit_files")]
-    can_read_files = [WorkflowPermission("read_files")]
-    can_update_files = [WorkflowPermission("update_files")]
-    can_delete_files = [WorkflowPermission("delete_files")]
-
-    can_read_draft = [WorkflowPermission("read_draft")]
-    can_update_draft = [WorkflowPermission("update_draft")]
-    can_delete_draft = [WorkflowPermission("delete_draft")]
-    can_edit = [WorkflowPermission("edit")]
-    can_new_version = [WorkflowPermission("new_version")]
-    can_draft_create_files = [WorkflowPermission("draft_create_files")]
-
-    can_search = [SystemProcess(), AnyUser()]
-    can_search_drafts = [SystemProcess(), AnyUser()]
-    can_search_versions = [SystemProcess(), AnyUser()]
-
-    @property
-    def query_filters(self) -> list[dict]:
-        if not (self.action == "read" or self.action == "read_draft"):
-            return super().query_filters
-        workflows = current_oarepo_workflows.record_workflows
-        queries = []
-        for workflow_id, workflow in workflows.items():
-            q_inworkflow = dsl.Q("term", **{"parent.workflow": workflow_id})
-            workflow_filters = workflow.permissions(
-                self.action, **self.over
-            ).query_filters
-            if not workflow_filters:
-                workflow_filters = [dsl.Q("match_none")]
-            query = reduce(lambda f1, f2: f1 | f2, workflow_filters) & q_inworkflow
-            queries.append(query)
-        return [q for q in queries if q]
diff --git a/oarepo_workflows/services/permissions/record_permission_policy.py b/oarepo_workflows/services/permissions/record_permission_policy.py
new file mode 100644
index 0000000..8d1ee24
--- /dev/null
+++ b/oarepo_workflows/services/permissions/record_permission_policy.py
@@ -0,0 +1,71 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Record policy for workflows."""
+
+from __future__ import annotations
+
+from functools import reduce
+
+from invenio_records_permissions import RecordPermissionPolicy
+from invenio_records_permissions.generators import (
+    AnyUser,
+    SystemProcess,
+)
+from invenio_search.engine import dsl
+
+from ...proxies import current_oarepo_workflows
+from .generators import WorkflowPermission
+
+
+class WorkflowRecordPermissionPolicy(RecordPermissionPolicy):
+    """Permission policy to be used in permission presets directly on RecordServiceConfig.permission_policy_cls.
+
+    Do not use this class in Workflow constructor.
+    """
+
+    can_create = [WorkflowPermission("create")]
+    can_publish = [WorkflowPermission("publish")]
+    can_read = [WorkflowPermission("read")]
+    can_update = [WorkflowPermission("update")]
+    can_delete = [WorkflowPermission("delete")]
+    can_create_files = [WorkflowPermission("create_files")]
+    can_set_content_files = [WorkflowPermission("set_content_files")]
+    can_get_content_files = [WorkflowPermission("get_content_files")]
+    can_commit_files = [WorkflowPermission("commit_files")]
+    can_read_files = [WorkflowPermission("read_files")]
+    can_update_files = [WorkflowPermission("update_files")]
+    can_delete_files = [WorkflowPermission("delete_files")]
+
+    can_read_draft = [WorkflowPermission("read_draft")]
+    can_update_draft = [WorkflowPermission("update_draft")]
+    can_delete_draft = [WorkflowPermission("delete_draft")]
+    can_edit = [WorkflowPermission("edit")]
+    can_new_version = [WorkflowPermission("new_version")]
+    can_draft_create_files = [WorkflowPermission("draft_create_files")]
+
+    can_search = [SystemProcess(), AnyUser()]
+    can_search_drafts = [SystemProcess(), AnyUser()]
+    can_search_versions = [SystemProcess(), AnyUser()]
+
+    @property
+    def query_filters(self) -> list[dict]:
+        """Return query filters from the delegated workflow permissions."""
+        if not (self.action == "read" or self.action == "read_draft"):
+            return super().query_filters
+        workflows = current_oarepo_workflows.record_workflows
+        queries = []
+        for workflow_id, workflow in workflows.items():
+            q_inworkflow = dsl.Q("term", **{"parent.workflow": workflow_id})
+            workflow_filters = workflow.permissions(
+                self.action, **self.over
+            ).query_filters
+            if not workflow_filters:
+                workflow_filters = [dsl.Q("match_none")]
+            query = reduce(lambda f1, f2: f1 | f2, workflow_filters) & q_inworkflow
+            queries.append(query)
+        return [q for q in queries if q]
diff --git a/oarepo_workflows/services/permissions/workflow_permissions.py b/oarepo_workflows/services/permissions/workflow_permissions.py
new file mode 100644
index 0000000..037b2d6
--- /dev/null
+++ b/oarepo_workflows/services/permissions/workflow_permissions.py
@@ -0,0 +1,71 @@
+from __future__ import annotations
+
+from typing import Optional
+
+from invenio_records_permissions import RecordPermissionPolicy
+from invenio_records_permissions.generators import (
+    AuthenticatedUser,
+    Disable,
+    SystemProcess,
+)
+from oarepo_runtime.services.permissions import RecordOwners
+
+from oarepo_workflows import IfInState
+from oarepo_workflows.services.permissions.generators import SameAs
+
+
+class DefaultWorkflowPermissions(RecordPermissionPolicy):
+    """Base class for workflow permissions, subclass from it and put the result to Workflow constructor.
+
+    Example:
+        class MyWorkflowPermissions(DefaultWorkflowPermissions):
+            can_read = [AnyUser()]
+    in invenio.cfg
+    WORKFLOWS = {
+        'default': Workflow(
+            permission_policy_cls = MyWorkflowPermissions, ...
+        )
+    }
+
+    """
+
+    # new version - update; edit current version - disable -> idk if there's other way than something like IfNoEditDraft/IfNoNewVersionDraft generators-
+
+    files_edit = [
+        IfInState("draft", [RecordOwners()]),
+        IfInState("published", [Disable()]),
+    ]
+
+    system_process = SystemProcess()
+
+    def __init__(self, action_name: Optional[str] = None, **over) -> None:
+        can = getattr(self, f"can_{action_name}")
+        if self.system_process not in can:
+            can.append(self.system_process)
+        over["policy"] = self
+        super().__init__(action_name, **over)
+
+    can_read = [
+        IfInState("draft", [RecordOwners()]),
+        IfInState("published", [AuthenticatedUser()]),
+    ]
+    can_update = [IfInState("draft", [RecordOwners()])]
+    can_delete = [
+        IfInState("draft", [RecordOwners()]),
+    ]
+    can_create = [AuthenticatedUser()]
+    can_publish = [AuthenticatedUser()]
+    can_new_version = [AuthenticatedUser()]
+
+    can_create_files = [SameAs("files_edit")]
+    can_set_content_files = [SameAs("files_edit")]
+    can_commit_files = [SameAs("files_edit")]
+    can_update_files = [SameAs("files_edit")]
+    can_delete_files = [SameAs("files_edit")]
+    can_draft_create_files = [SameAs("files_edit")]
+    can_read_files = [SameAs("can_read")]
+    can_get_content_files = [SameAs("can_read")]
+
+    can_read_draft = [SameAs("can_read")]
+    can_update_draft = [SameAs("can_update")]
+    can_delete_draft = [SameAs("can_delete")]
diff --git a/oarepo_workflows/services/records/__init__.py b/oarepo_workflows/services/records/__init__.py
index e69de29..892225a 100644
--- a/oarepo_workflows/services/records/__init__.py
+++ b/oarepo_workflows/services/records/__init__.py
@@ -0,0 +1,7 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
diff --git a/oarepo_workflows/services/records/schema.py b/oarepo_workflows/services/records/schema.py
index 5c8f137..a3fea29 100644
--- a/oarepo_workflows/services/records/schema.py
+++ b/oarepo_workflows/services/records/schema.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from __future__ import annotations
 
 import marshmallow as ma
diff --git a/oarepo_workflows/views/__init__.py b/oarepo_workflows/views/__init__.py
index e69de29..892225a 100644
--- a/oarepo_workflows/views/__init__.py
+++ b/oarepo_workflows/views/__init__.py
@@ -0,0 +1,7 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index d62ea5c..e2d97cf 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from __future__ import annotations
 
 from flask import Blueprint
@@ -10,6 +17,7 @@ def create_api_blueprint(app) -> Blueprint:
 
     def register_autoapprove_entity_resolver(state) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
+
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())
 
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index 969eede..c751291 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from __future__ import annotations
 
 from flask import Blueprint
@@ -10,6 +17,7 @@ def create_app_blueprint(app) -> Blueprint:
 
     def register_autoapprove_entity_resolver(state) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
+
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())
 
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..6a1c41c
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,26 @@
+[lint]
+extend-select = [
+    "UP",   # pyupgrade
+    "D",    # pydocstyle
+    "B",    # flake8-bugbear
+    "SIM",  # flake8-simplify
+    "I",    # isort
+    "TCH",  # type checking
+    "ANN",  # annotations
+    "DOC",  # docstrings
+]
+
+ignore = [
+    "ANN101",  # Missing type annotation for self in method
+    "ANN102",  # Missing type annotation for cls in classmethod
+    "ANN204",  # Missing return type annotation in __init__ method
+    "ANN401",  # we are using Any in kwargs, so ignore those
+    "UP007",   # Imho a: Optional[int] = None is more readable than a: (int | None) = None for kwargs
+
+    "D203",    # 1 blank line required before class docstring (we use D211)
+    "D213",    # Multi-line docstring summary should start at the second line - we use D212 (starting on the same line)
+    "D404",    # First word of the docstring should not be This
+]
+
+[lint.flake8-annotations]
+mypy-init-return = true
diff --git a/setup.cfg b/setup.cfg
index 84900c5..2de0b95 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -9,7 +9,7 @@ long_description_content_type = text/markdown
 
 
 [options]
-python = >=3.9
+python = >=3.12
 install_requires =
     invenio-records-resources
     invenio-requests
diff --git a/setup.py b/setup.py
index 6068493..a8aef14 100644
--- a/setup.py
+++ b/setup.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from setuptools import setup
 
 setup()
diff --git a/tests/__init__.py b/tests/__init__.py
index e69de29..892225a 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -0,0 +1,7 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
diff --git a/tests/conftest.py b/tests/conftest.py
index 33cc8a4..7e4b2e0 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 import os
 
 import pytest
@@ -25,7 +32,6 @@ class RecordOwnersReadTestWorkflowPermissionPolicy(DefaultWorkflowPermissions):
 
 
 class Administration(Generator):
-
     def needs(self, **kwargs):
         """Enabling Needs."""
         return [ActionNeed("administration")]
@@ -128,7 +134,6 @@ def input_data(sample_metadata_list):
 
 @pytest.fixture()
 def users(app, db, UserFixture):
-
     user1 = UserFixture(
         email="user1@example.org",
         password="password",
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index a98d784..00111bd 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 import pytest
 
 from oarepo_workflows.errors import InvalidWorkflowError
diff --git a/tests/test_workflow_field.py b/tests/test_workflow_field.py
index 4c87275..9cf5144 100644
--- a/tests/test_workflow_field.py
+++ b/tests/test_workflow_field.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from invenio_access.permissions import system_identity
 
 
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index b12e975..cb11e2b 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from types import SimpleNamespace
 
 from invenio_records_permissions.generators import Generator
@@ -36,21 +43,23 @@ def test_request_policy_access(app):
     assert getattr(request_policy, "delete_request", None)
     assert not getattr(request_policy, "non_existing_request", None)
 
+
 def test_is_applicable(users, logged_client, search_clear, record_service):
     req = WorkflowRequest(
         requesters=[RecordOwners()],
         recipients=[NullRecipient(), TestRecipient()],
     )
-    id1=Identity(id=1)
+    id1 = Identity(id=1)
     id1.provides.add(UserNeed(1))
 
-    id2=Identity(id=2)
+    id2 = Identity(id=2)
     id2.provides.add(UserNeed(2))
 
-    record = SimpleNamespace(parent=SimpleNamespace(owners = [SimpleNamespace(id=1)]))
+    record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
     assert req.is_applicable(id2, record=record) is False
     assert req.is_applicable(id1, record=record) is True
 
+
 def test_list_applicable_requests(users, logged_client, search_clear, record_service):
     class R(WorkflowRequestPolicy):
         req = WorkflowRequest(
@@ -66,15 +75,19 @@ class R(WorkflowRequestPolicy):
 
     requests = R()
 
-    id1=Identity(id=1)
+    id1 = Identity(id=1)
     id1.provides.add(UserNeed(1))
 
-    id2=Identity(id=2)
+    id2 = Identity(id=2)
     id2.provides.add(UserNeed(2))
     id2.provides.add(RoleNeed("administrator"))
 
-    record = SimpleNamespace(parent=SimpleNamespace(owners = [SimpleNamespace(id=1)]))
+    record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
 
-    assert set(x[0] for x in requests.applicable_workflow_requests(id1, record=record)) == {'req'}
+    assert set(
+        x[0] for x in requests.applicable_workflow_requests(id1, record=record)
+    ) == {"req"}
 
-    assert set(x[0] for x in requests.applicable_workflow_requests(id2, record=record)) == {'req1'}
+    assert set(
+        x[0] for x in requests.applicable_workflow_requests(id2, record=record)
+    ) == {"req1"}

From 491850f976cfa0d058847621e3278e9983023ee9 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 15 Nov 2024 21:10:21 +0100
Subject: [PATCH 07/21] Added documentation and annotations

---
 oarepo_workflows/errors.py                    | 10 +++++++---
 oarepo_workflows/ext.py                       |  6 +++---
 oarepo_workflows/ext_config.py                |  7 ++++++-
 .../records/systemfields/state.py             |  4 ++--
 oarepo_workflows/requests/events.py           |  2 +-
 oarepo_workflows/requests/generators.py       | 12 +++++++++---
 .../services/components/workflow.py           |  8 +++++---
 .../services/permissions/generators.py        | 19 ++++++++++++-------
 .../permissions/workflow_permissions.py       | 14 ++++++++++++--
 oarepo_workflows/services/records/__init__.py |  1 +
 oarepo_workflows/services/records/schema.py   |  4 ++++
 oarepo_workflows/views/__init__.py            |  1 +
 oarepo_workflows/views/api.py                 |  9 +++++----
 oarepo_workflows/views/app.py                 |  9 +++++----
 setup.py                                      |  2 ++
 15 files changed, 75 insertions(+), 33 deletions(-)

diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index c211771..ff440fb 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING
+from typing import TYPE_CHECKING, Optional
 
 from marshmallow import ValidationError
 
@@ -24,7 +24,11 @@ def _get_id_from_record(record: Record | dict) -> str:
     :return str: The id of the record.
     """
     # community record doesn't have id in dict form, only uuid
-    return str(record["id"]) if "id" in record else str(record.id)
+    if "id" in record:
+        return str(record["id"])
+    if hasattr(record, "id"):
+        return str(record.id)
+    raise ValueError(f"Record {record} doesn't have an id.")
 
 
 def _format_record(record: Record | dict) -> str:
@@ -55,7 +59,7 @@ def __init__(
         self,
         message: str,
         record: Record | dict | None = None,
-        community_id: str = None,
+        community_id: Optional[str] = None,
     ) -> None:
         """Initialize the exception."""
         self.record = record
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 96518a4..b0dd6b1 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -10,7 +10,7 @@
 from __future__ import annotations
 
 from functools import cached_property
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
 
 import importlib_metadata
 from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
@@ -41,7 +41,7 @@
 class OARepoWorkflows:
     """OARepo workflows extension."""
 
-    def __init__(self, app: Flask = None) -> None:
+    def __init__(self, app: Optional[Flask] = None) -> None:
         """Initialize the extension.
 
         :param app: Flask application to initialize with.
@@ -50,7 +50,7 @@ def __init__(self, app: Flask = None) -> None:
         if app:
             self.init_config(app)
             self.init_app(app)
-            self.init_services(app)
+            self.init_services()
 
     def init_config(self, app: Flask) -> None:
         """Initialize configuration.
diff --git a/oarepo_workflows/ext_config.py b/oarepo_workflows/ext_config.py
index 62b9ba3..36c1c76 100644
--- a/oarepo_workflows/ext_config.py
+++ b/oarepo_workflows/ext_config.py
@@ -9,14 +9,19 @@
 
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from oarepo_workflows.services.permissions.record_permission_policy import (
     WorkflowRecordPermissionPolicy,
 )
 
+if TYPE_CHECKING:
+    from oarepo_workflows import Workflow
+
 OAREPO_PERMISSIONS_PRESETS = {
     "workflow": WorkflowRecordPermissionPolicy,
 }
 """Permissions presets for oarepo-workflows."""
 
-WORKFLOWS = {}
+WORKFLOWS: dict[str, Workflow] = {}
 """Configuration of workflows, must be provided by the user inside, for example, invenio.cfg."""
diff --git a/oarepo_workflows/records/systemfields/state.py b/oarepo_workflows/records/systemfields/state.py
index d66071d..2002a09 100644
--- a/oarepo_workflows/records/systemfields/state.py
+++ b/oarepo_workflows/records/systemfields/state.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from typing import Any, Protocol, Self, overload
+from typing import Any, Optional, Protocol, Self, overload
 
 from invenio_records.systemfields.base import SystemField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
@@ -40,7 +40,7 @@ def post_create(self, record: WithState) -> None:
         self.set_dictkey(record, self._initial)
 
     def post_init(
-        self, record: WithState, data: dict, model: Any = None, **kwargs: Any
+        self, record: WithState, data: dict, model: Optional[Any] = None, **kwargs: Any
     ) -> None:
         """Set the initial state when record is created."""
         if not record.state:
diff --git a/oarepo_workflows/requests/events.py b/oarepo_workflows/requests/events.py
index f80e0fb..012bc5e 100644
--- a/oarepo_workflows/requests/events.py
+++ b/oarepo_workflows/requests/events.py
@@ -13,7 +13,7 @@
 from functools import cached_property
 from typing import TYPE_CHECKING
 
-from oarepo_workflows.requests import MultipleGeneratorsGenerator
+from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
 
 if TYPE_CHECKING:
     from invenio_records_permissions.generators import Generator
diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators.py
index 4510504..a8fd8cc 100644
--- a/oarepo_workflows/requests/generators.py
+++ b/oarepo_workflows/requests/generators.py
@@ -10,7 +10,7 @@
 from __future__ import annotations
 
 import dataclasses
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
 
 from invenio_access import SystemRoleNeed
 from invenio_records_permissions.generators import Generator
@@ -85,7 +85,10 @@ class RecipientGeneratorMixin:
     """Mixin for permission generators that can be used as recipients in WorkflowRequest."""
 
     def reference_receivers(
-        self, record: Record = None, request_type: RequestType = None, **context: Any
+        self,
+        record: Optional[Record] = None,
+        request_type: Optional[RequestType] = None,
+        **context: Any,
     ) -> list[dict[str, str]]:
         """Return the reference receiver(s) of the request.
 
@@ -107,7 +110,10 @@ class AutoApprove(RecipientGeneratorMixin, Generator):
     """
 
     def reference_receivers(
-        self, record: Record = None, request_type: RequestType = None, **kwargs: Any
+        self,
+        record: Optional[Record] = None,
+        request_type: Optional[RequestType] = None,
+        **kwargs: Any,
     ) -> list[dict[str, str]]:
         """Return the reference receiver(s) of the auto-approve request.
 
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 63669e9..4acf5e3 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Any
+from typing import TYPE_CHECKING, Any, Optional
 
 from invenio_records_resources.services.records.components.base import ServiceComponent
 
@@ -31,11 +31,13 @@ class WorkflowComponent(ServiceComponent):
     def create(
         self,
         identity: Identity,
-        data: dict = None,
-        record: Record = None,
+        data: Optional[dict] = None,
+        record: Optional[Record] = None,
         **kwargs: Any,
     ) -> None:
         """Implement record creation checks and set the workflow on the created record."""
+        if not data:
+            raise MissingWorkflowError("Workflow not defined in input.", record=data)
         try:
             workflow_id = data["parent"]["workflow"]
         except KeyError as e:
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 6895560..d71e894 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -12,7 +12,7 @@
 import operator
 from functools import reduce
 from itertools import chain
-from typing import TYPE_CHECKING, Any, Iterable
+from typing import TYPE_CHECKING, Any, Iterable, Optional
 
 from invenio_records_permissions.generators import ConditionalGenerator, Generator
 from invenio_search.engine import dsl
@@ -27,7 +27,7 @@
 
 
 # invenio_records_permissions.generators.ConditionalGenerator._make_query
-def _make_query(generators: Iterable[Generator], **context: Any) -> dict:
+def _make_query(generators: Iterable[Generator], **context: Any) -> dict | None:
     queries = [g.query_filter(**context) for g in generators]
     queries = [q for q in queries if q]
     return reduce(operator.or_, queries) if queries else None
@@ -49,7 +49,7 @@ def __init__(self, action: str | None = None) -> None:
         super().__init__()
         self._action = action
 
-    def _get_workflow_id(self, record: Record = None, **context: Any) -> str:
+    def _get_workflow_id(self, record: Optional[Record] = None, **context: Any) -> str:
         """Get the workflow id from the context.
 
         If the record is passed, the workflow is determined from the record.
@@ -78,7 +78,10 @@ def _get_workflow_id(self, record: Record = None, **context: Any) -> str:
         return workflow_id
 
     def _get_permissions_from_workflow(
-        self, record: Record = None, action_name: str | None = None, **context: Any
+        self,
+        record: Optional[Record] = None,
+        action_name: str | None = None,
+        **context: Any,
     ) -> RecordPermissionPolicy:
         """Get the permissions policy from the workflow.
 
@@ -96,13 +99,13 @@ def _get_permissions_from_workflow(
         policy = current_oarepo_workflows.record_workflows[workflow_id].permissions
         return policy(action_name, record=record, **context)
 
-    def needs(self, record: Record = None, **context: Any) -> set[Need]:
+    def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
         """Return needs that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(
             record, self._action, **context
         ).needs
 
-    def query_filter(self, record: Record = None, **context: Any) -> dict:
+    def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
         """Return query filters that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(
             record, self._action, **context
@@ -196,6 +199,8 @@ def excludes(self, policy: RecordPermissionPolicy, **context: Any) -> set[Need]:
         ]
         return set(chain.from_iterable(excludes))
 
-    def query_filter(self, policy: RecordPermissionPolicy, **context: Any) -> dict:
+    def query_filter(
+        self, policy: RecordPermissionPolicy, **context: Any
+    ) -> dict | None:
         """Search filters."""
         return _make_query(self._generators(policy, **context), **context)
diff --git a/oarepo_workflows/services/permissions/workflow_permissions.py b/oarepo_workflows/services/permissions/workflow_permissions.py
index 037b2d6..820b187 100644
--- a/oarepo_workflows/services/permissions/workflow_permissions.py
+++ b/oarepo_workflows/services/permissions/workflow_permissions.py
@@ -1,6 +1,15 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Definition of workflow permissions."""
+
 from __future__ import annotations
 
-from typing import Optional
+from typing import Any
 
 from invenio_records_permissions import RecordPermissionPolicy
 from invenio_records_permissions.generators import (
@@ -38,7 +47,8 @@ class MyWorkflowPermissions(DefaultWorkflowPermissions):
 
     system_process = SystemProcess()
 
-    def __init__(self, action_name: Optional[str] = None, **over) -> None:
+    def __init__(self, action_name: str | None = None, **over: Any) -> None:
+        """Initialize the workflow permissions."""
         can = getattr(self, f"can_{action_name}")
         if self.system_process not in can:
             can.append(self.system_process)
diff --git a/oarepo_workflows/services/records/__init__.py b/oarepo_workflows/services/records/__init__.py
index 892225a..52b293a 100644
--- a/oarepo_workflows/services/records/__init__.py
+++ b/oarepo_workflows/services/records/__init__.py
@@ -5,3 +5,4 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""Service layer for workflow-enabled records."""
diff --git a/oarepo_workflows/services/records/schema.py b/oarepo_workflows/services/records/schema.py
index a3fea29..de75b07 100644
--- a/oarepo_workflows/services/records/schema.py
+++ b/oarepo_workflows/services/records/schema.py
@@ -5,6 +5,8 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""Mixin for records with workflow support."""
+
 from __future__ import annotations
 
 import marshmallow as ma
@@ -12,4 +14,6 @@
 
 
 class WorkflowParentSchema(ParentSchema):
+    """Schema for parent record with workflow support."""
+
     workflow = ma.fields.String()
diff --git a/oarepo_workflows/views/__init__.py b/oarepo_workflows/views/__init__.py
index 892225a..240ba96 100644
--- a/oarepo_workflows/views/__init__.py
+++ b/oarepo_workflows/views/__init__.py
@@ -5,3 +5,4 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""Registered blueprints for API/UI."""
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index e2d97cf..d2d12cd 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -5,17 +5,18 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""API blueprints."""
+
 from __future__ import annotations
 
-from flask import Blueprint
-from flask.blueprints import Blueprint
+from flask import Blueprint, BlueprintSetupState, Flask
 
 
-def create_api_blueprint(app) -> Blueprint:
+def create_api_blueprint(app: Flask) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state) -> None:
+    def register_autoapprove_entity_resolver(state: BlueprintSetupState) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
 
         requests = app.extensions["invenio-requests"]
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index c751291..e3af857 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -5,17 +5,18 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""App blueprints."""
+
 from __future__ import annotations
 
-from flask import Blueprint
-from flask.blueprints import Blueprint
+from flask.blueprints import Blueprint, BlueprintSetupState, Flask
 
 
-def create_app_blueprint(app) -> Blueprint:
+def create_app_blueprint(app: Flask) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state) -> None:
+    def register_autoapprove_entity_resolver(state: BlueprintSetupState) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
 
         requests = app.extensions["invenio-requests"]
diff --git a/setup.py b/setup.py
index a8aef14..c07b204 100644
--- a/setup.py
+++ b/setup.py
@@ -5,6 +5,8 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
+"""Setup module for oarepo_workflows."""
+
 from setuptools import setup
 
 setup()

From 43cd9c0b5e9126789b25e95db1d4de0fd185531d Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Mon, 18 Nov 2024 05:38:54 +0100
Subject: [PATCH 08/21] Working tests, updated docs, typing

---
 oarepo_workflows/errors.py                    |  2 +-
 oarepo_workflows/ext.py                       |  4 ++-
 .../services/permissions/generators.py        | 31 +++++++++++--------
 .../permissions/workflow_permissions.py       |  3 +-
 oarepo_workflows/views/api.py                 |  7 ++++-
 oarepo_workflows/views/app.py                 |  7 ++++-
 run-tests.sh                                  | 10 +++---
 tests/test_workflow.py                        |  2 +-
 8 files changed, 42 insertions(+), 24 deletions(-)

diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index ff440fb..cc6d159 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -28,7 +28,7 @@ def _get_id_from_record(record: Record | dict) -> str:
         return str(record["id"])
     if hasattr(record, "id"):
         return str(record.id)
-    raise ValueError(f"Record {record} doesn't have an id.")
+    return str(record)
 
 
 def _format_record(record: Record | dict) -> str:
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index b0dd6b1..d7c7eae 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -14,7 +14,7 @@
 
 import importlib_metadata
 from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
-from invenio_records_resources.services.uow import RecordCommitOp
+from invenio_records_resources.services.uow import RecordCommitOp, unit_of_work
 from oarepo_runtime.datastreams.utils import get_record_service_for_record
 
 from oarepo_workflows.errors import InvalidWorkflowError, MissingWorkflowError
@@ -102,6 +102,7 @@ def workflow_changed_notifiers(self) -> list[WorkflowChangeNotifier]:
             x.load() for x in importlib_metadata.entry_points().select(group=group_name)
         ]
 
+    @unit_of_work()
     def set_state(
         self,
         identity: Identity,
@@ -132,6 +133,7 @@ def set_state(
                 identity, record, previous_value, new_state, *args, uow=uow, **kwargs
             )
 
+    @unit_of_work()
     def set_workflow(
         self,
         identity: Identity,
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index d71e894..2074400 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -173,34 +173,39 @@ class Perms:
 
     """
 
-    def __init__(self, action: str) -> None:
-        """Initialize the generator."""
-        self.action_generator_name = f"can_{action}"
+    def __init__(self, permission_name: str) -> None:
+        """Initialize the generator.
+
+        :param permission_name: Name of the permission to delegate to. In most cases,
+        it will look like "can_<action>". A property with this name must exist on the policy
+        and its value must be a list of generators.
+        """
+        self.delegated_permission_name = permission_name
 
     def _generators(
-        self, policy: RecordPermissionPolicy, **context: Any
+        self, *, policy: RecordPermissionPolicy, **context: Any
     ) -> list[Generator]:
         """Get the generators from the policy."""
-        return getattr(policy, self.action_generator_name)
+        return getattr(policy, self.delegated_permission_name)
 
-    def needs(self, policy: RecordPermissionPolicy, **context: Any) -> set[Need]:
+    def needs(self, *, policy: RecordPermissionPolicy, **context: Any) -> list[Need]:
         """Get the needs from the policy."""
         needs = [
             generator.needs(**context)
-            for generator in self._generators(policy, **context)
+            for generator in self._generators(policy=policy, **context)
         ]
-        return set(chain.from_iterable(needs))
+        return list(chain.from_iterable(needs))
 
-    def excludes(self, policy: RecordPermissionPolicy, **context: Any) -> set[Need]:
+    def excludes(self, *, policy: RecordPermissionPolicy, **context: Any) -> list[Need]:
         """Get the excludes from the policy."""
         excludes = [
             generator.excludes(**context)
-            for generator in self._generators(policy, **context)
+            for generator in self._generators(policy=policy, **context)
         ]
-        return set(chain.from_iterable(excludes))
+        return list(chain.from_iterable(excludes))
 
     def query_filter(
-        self, policy: RecordPermissionPolicy, **context: Any
+        self, *, policy: RecordPermissionPolicy, **context: Any
     ) -> dict | None:
         """Search filters."""
-        return _make_query(self._generators(policy, **context), **context)
+        return _make_query(self._generators(policy=policy, **context), **context)
diff --git a/oarepo_workflows/services/permissions/workflow_permissions.py b/oarepo_workflows/services/permissions/workflow_permissions.py
index 820b187..1ec44d9 100644
--- a/oarepo_workflows/services/permissions/workflow_permissions.py
+++ b/oarepo_workflows/services/permissions/workflow_permissions.py
@@ -19,8 +19,7 @@
 )
 from oarepo_runtime.services.permissions import RecordOwners
 
-from oarepo_workflows import IfInState
-from oarepo_workflows.services.permissions.generators import SameAs
+from .generators import IfInState, SameAs
 
 
 class DefaultWorkflowPermissions(RecordPermissionPolicy):
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index d2d12cd..e788ca9 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -9,7 +9,12 @@
 
 from __future__ import annotations
 
-from flask import Blueprint, BlueprintSetupState, Flask
+from typing import TYPE_CHECKING
+
+from flask import Blueprint, Flask
+
+if TYPE_CHECKING:
+    from flask.blueprints import BlueprintSetupState
 
 
 def create_api_blueprint(app: Flask) -> Blueprint:
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index e3af857..c090bea 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -9,7 +9,12 @@
 
 from __future__ import annotations
 
-from flask.blueprints import Blueprint, BlueprintSetupState, Flask
+from typing import TYPE_CHECKING
+
+from flask import Blueprint, Flask
+
+if TYPE_CHECKING:
+    from flask.blueprints import BlueprintSetupState
 
 
 def create_app_blueprint(app: Flask) -> Blueprint:
diff --git a/run-tests.sh b/run-tests.sh
index ba2c7b3..3c5fd8a 100755
--- a/run-tests.sh
+++ b/run-tests.sh
@@ -1,8 +1,10 @@
 #!/bin/bash
 
-PYTHON="${PYTHON:-python3.10}"
+PYTHON="${PYTHON:-python3.12}"
 OAREPO_VERSION="${OAREPO_VERSION:-12}"
 
+echo "Will run tests with python ${PYTHON} and oarepo version ${OAREPO_VERSION}"
+
 set -e
 
 install_python_package() {
@@ -20,9 +22,6 @@ if test -d $BUILDER_VENV ; then
 	rm -rf $BUILDER_VENV
 fi
 curl -L -o forked_install.sh https://github.com/oarepo/nrp-devtools/raw/main/tests/forked_install.sh
-if test -d $TESTS_VENV ; then
-	rm -rf $TESTS_VENV
-fi
 
 $PYTHON -m venv $BUILDER_VENV
 . $BUILDER_VENV/bin/activate
@@ -35,6 +34,9 @@ if test -d thesis ; then
 fi
 oarepo-compile-model ./tests/thesis.yaml --output-directory ./thesis -vvv
 
+if test -d $TESTS_VENV ; then
+	rm -rf $TESTS_VENV
+fi
 $PYTHON -m venv $TESTS_VENV
 . $TESTS_VENV/bin/activate
 pip install -U setuptools pip wheel
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 00111bd..d1ff0d2 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -110,7 +110,7 @@ def test_invalid_workflow_input(users, logged_client, search_clear):
     )
     assert invalid_wf_response.status_code == 400
     assert invalid_wf_response.json["errors"][0]["messages"] == [
-        "Workflow rglknjgidlrg does not exist in the configuration."
+        "Workflow rglknjgidlrg does not exist in the configuration. Used on record dict[{'parent': {'workflow': 'rglknjgidlrg'}}]"
     ]
     missing_wf_response = user_client1.post(ThesisResourceConfig.url_prefix, json={})
     assert missing_wf_response.status_code == 400

From eb640be0b8c3076f18f94fc32e6630cb12d91824 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Mon, 18 Nov 2024 11:03:45 +0100
Subject: [PATCH 09/21] More tests

---
 .coveragerc                                   |  4 +
 oarepo_workflows/errors.py                    |  7 +-
 oarepo_workflows/requests/generators.py       | 36 +++++--
 oarepo_workflows/requests/requests.py         |  8 +-
 .../services/components/workflow.py           | 12 ++-
 .../services/permissions/generators.py        |  6 +-
 ruff.toml                                     |  4 +
 tests/test_auto_approve.py                    | 33 +++++++
 tests/test_auto_request.py                    |  6 ++
 tests/test_if_in_state.py                     |  0
 tests/test_workflow.py                        | 10 ++
 tests/test_workflow_permission.py             | 28 ++++++
 tests/test_workflow_requests.py               | 93 ++++++++++++++++---
 13 files changed, 213 insertions(+), 34 deletions(-)
 create mode 100644 .coveragerc
 create mode 100644 tests/test_auto_approve.py
 create mode 100644 tests/test_auto_request.py
 create mode 100644 tests/test_if_in_state.py
 create mode 100644 tests/test_workflow_permission.py

diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..1bd1b83
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[report]
+exclude_lines =
+    pragma: no cover
+    if TYPE_CHECKING:
\ No newline at end of file
diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index cc6d159..d01643f 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -24,8 +24,11 @@ def _get_id_from_record(record: Record | dict) -> str:
     :return str: The id of the record.
     """
     # community record doesn't have id in dict form, only uuid
-    if "id" in record:
-        return str(record["id"])
+    try:
+        if "id" in record:
+            return str(record["id"])
+    except TypeError:
+        pass
     if hasattr(record, "id"):
         return str(record.id)
     return str(record)
diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators.py
index a8fd8cc..51633cf 100644
--- a/oarepo_workflows/requests/generators.py
+++ b/oarepo_workflows/requests/generators.py
@@ -10,6 +10,7 @@
 from __future__ import annotations
 
 import dataclasses
+from collections.abc import Iterable
 from typing import TYPE_CHECKING, Any, Optional
 
 from invenio_access import SystemRoleNeed
@@ -50,7 +51,7 @@ def excludes(self, **context: Any) -> set[Need]:
             for exclude in generator.excludes(**context)
         }
 
-    def query_filters(self, **context: Any) -> list[dict]:
+    def query_filter(self, **context: Any) -> list[dict]:
         """Generate a list of opensearch query filters.
 
          These filters are used to filter objects. These objects are governed by a policy
@@ -58,11 +59,15 @@ def query_filters(self, **context: Any) -> list[dict]:
 
         :param context: Context.
         """
-        return [
-            query_filter
-            for generator in self.generators
-            for query_filter in generator.query_filter(**context)
-        ]
+        ret = []
+        for generator in self.generators:
+            query_filter = generator.query_filter(**context)
+            if query_filter:
+                if isinstance(query_filter, Iterable):
+                    ret.extend(query_filter)
+                else:
+                    ret.append(query_filter)
+        return ret
 
 
 auto_request_need = SystemRoleNeed("auto_request")
@@ -89,7 +94,7 @@ def reference_receivers(
         record: Optional[Record] = None,
         request_type: Optional[RequestType] = None,
         **context: Any,
-    ) -> list[dict[str, str]]:
+    ) -> list[dict[str, str]]: # pragma: no cover
         """Return the reference receiver(s) of the request.
 
         This call requires the context to contain at least "record" and "request_type"
@@ -119,4 +124,19 @@ def reference_receivers(
 
         Returning "auto_approve" is a signal to the workflow that the request should be auto-approved.
         """
-        return [{"auto_approve": "true"}]
+        return [{"auto_approve": "True"}]
+
+    def needs(self, **kwargs):
+        """Get needs that signal workflow to automatically approve the request."""
+        raise ValueError("Auto-approve generator can not create needs and "
+                         "should be used only in `recipient` section of WorkflowRequest.")
+
+    def excludes(self, **kwargs):
+        """Get needs that signal workflow to automatically approve the request."""
+        raise ValueError("Auto-approve generator can not create needs and "
+                         "should be used only in `recipient` section of WorkflowRequest.")
+
+    def query_filter(self, **kwargs):
+        """Get needs that signal workflow to automatically approve the request."""
+        raise ValueError("Auto-approve generator can not create needs and "
+                         "should be used only in `recipient` section of WorkflowRequest.")
\ No newline at end of file
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index c5bdbd7..c006798 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -109,13 +109,11 @@ class WorkflowTransitions:
 
     def __getitem__(self, transition_name: str):
         """Get the transition by name."""
-        try:
-            return getattr(self, transition_name)
-        except AttributeError:
+        if transition_name not in ["submitted", "accepted", "declined"]:
             raise KeyError(
                 f"Transition {transition_name} not defined in {self.__class__.__name__}"
-            ) from None
-
+            )
+        return getattr(self, transition_name)
 
 @dataclasses.dataclass
 class WorkflowRequestEscalation:
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index 4acf5e3..d1dedee 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -37,12 +37,16 @@ def create(
     ) -> None:
         """Implement record creation checks and set the workflow on the created record."""
         if not data:
-            raise MissingWorkflowError("Workflow not defined in input.", record=data)
+            # sanity check, should be handled by policy before the component is called
+            raise MissingWorkflowError("Workflow not defined in input. As this should be handled by a policy, "
+                                       "make sure you are using workflow-enabled policy.", record=data)   # pragma: no cover
         try:
             workflow_id = data["parent"]["workflow"]
-        except KeyError as e:
-            raise MissingWorkflowError(
-                "Workflow not defined in input.", record=data
+        except KeyError as e:               # pragma: no cover
+            # sanity check, should be handled by policy before the component is called
+            raise MissingWorkflowError(     # pragma: no cover
+                "Workflow not defined in input. As this should be handled by a policy, "
+                "make sure you are using workflow-enabled policy.", record=data
             ) from e
 
         current_oarepo_workflows.set_workflow(
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 2074400..028c38e 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -106,7 +106,11 @@ def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
         ).needs
 
     def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
-        """Return query filters that are generated by the workflow permission."""
+        """Return query filters that are generated by the workflow permission.
+
+           Note: this implementation in fact will be called from WorkflowRecordPermissionPolicy.query_filters
+           for each registered workflow type. The query_filters are then combined into a single query.
+        """
         return self._get_permissions_from_workflow(
             record, self._action, **context
         ).query_filters
diff --git a/ruff.toml b/ruff.toml
index 6a1c41c..e0da368 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,3 +1,7 @@
+exclude = [
+    "tests"
+]
+
 [lint]
 extend-select = [
     "UP",   # pyupgrade
diff --git a/tests/test_auto_approve.py b/tests/test_auto_approve.py
new file mode 100644
index 0000000..645ad87
--- /dev/null
+++ b/tests/test_auto_approve.py
@@ -0,0 +1,33 @@
+from oarepo_workflows.requests.generators import AutoApprove, auto_approve_need
+import pytest
+from invenio_requests.resolvers.registry import ResolverRegistry
+
+from oarepo_workflows.resolvers.auto_approve import AutoApproveProxy, AutoApproveEntity
+from invenio_access.permissions import system_identity
+
+
+def test_auto_approve_generator():
+    a = AutoApprove()
+
+    with pytest.raises(ValueError):
+        a.needs()
+
+    with pytest.raises(ValueError):
+        a.excludes()
+
+    with pytest.raises(ValueError):
+        a.query_filter()
+
+    assert a.reference_receivers() == [{"auto_approve": "True"}]
+
+
+def test_auto_approve_resolver(app):
+    resolved = ResolverRegistry.resolve_entity({"auto_approve": "True"})
+    assert isinstance(resolved, AutoApproveEntity)
+    #
+    # assert resolved.resolve() == AutoApprove()
+    # assert resolved.pick_resolved_fields(system_identity, {"id": "true"}) == {"auto_approve": "true"}
+    # assert resolved.get_needs() == []
+
+    entity_reference = ResolverRegistry.reference_entity(AutoApproveEntity())
+    assert entity_reference == {"auto_approve": "True"}
diff --git a/tests/test_auto_request.py b/tests/test_auto_request.py
new file mode 100644
index 0000000..c4a0e00
--- /dev/null
+++ b/tests/test_auto_request.py
@@ -0,0 +1,6 @@
+from oarepo_workflows import AutoRequest
+from oarepo_workflows.requests.generators import auto_request_need
+
+
+def test_auto_request_needs(app):
+    assert AutoRequest().needs() == [auto_request_need]
\ No newline at end of file
diff --git a/tests/test_if_in_state.py b/tests/test_if_in_state.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index d1ff0d2..7fb5d61 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -12,6 +12,16 @@
 from thesis.thesis.records.api import ThesisDraft, ThesisRecord
 
 
+def test_create_without_workflow(users, logged_client, default_workflow_json, search_clear):
+    # create draft
+    user_client1 = logged_client(users[0])
+
+    create_response = user_client1.post(
+        ThesisResourceConfig.url_prefix, json={}
+    )
+    assert create_response.status_code == 400
+    assert create_response.json["errors"][0]["messages"] == ['Workflow not defined in input.']
+
 def test_workflow_read(users, logged_client, default_workflow_json, search_clear):
     # create draft
     user_client1 = logged_client(users[0])
diff --git a/tests/test_workflow_permission.py b/tests/test_workflow_permission.py
new file mode 100644
index 0000000..41cb5d8
--- /dev/null
+++ b/tests/test_workflow_permission.py
@@ -0,0 +1,28 @@
+from types import SimpleNamespace
+
+from oarepo_workflows import WorkflowPermission
+from oarepo_workflows.errors import MissingWorkflowError
+from thesis.thesis.records.api import ThesisRecord
+import pytest
+
+from flask_principal import Identity, UserNeed
+
+
+def test_get_workflow_id(users, logged_client, search_clear, record_service):
+    thesis = ThesisRecord.create({})
+    wp = WorkflowPermission("read")
+    with pytest.raises(MissingWorkflowError):
+        wp._get_workflow_id(record=thesis)
+
+    fake_thesis = SimpleNamespace(parent=SimpleNamespace(workflow=""))
+    with pytest.raises(MissingWorkflowError):
+        assert wp._get_workflow_id(record=fake_thesis)  # noqa
+
+def test_query_filter(users, logged_client, search_clear, record_service):
+    wp = WorkflowPermission("read")
+
+    id1 = Identity(id=1)
+    id1.provides.add(UserNeed(1))
+
+    with pytest.raises(MissingWorkflowError):
+        assert wp.query_filter(identity=id1) == {}
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index cb11e2b..1579cb2 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -8,14 +8,15 @@
 from types import SimpleNamespace
 
 from invenio_records_permissions.generators import Generator
-from oarepo_runtime.services.generators import RecordOwners
+from oarepo_runtime.services.permissions import RecordOwners, UserWithRole
 
-from oarepo_workflows import WorkflowRequestPolicy
+from oarepo_workflows import WorkflowRequestPolicy, WorkflowTransitions
 from oarepo_workflows.requests import RecipientGeneratorMixin, WorkflowRequest
 from thesis.thesis.records.api import ThesisRecord
 
 from flask_principal import Identity, UserNeed, RoleNeed
-from oarepo_runtime.services.permissions.generators import UserWithRole
+import pytest
+from opensearch_dsl.query import Terms
 
 
 class TestRecipient(RecipientGeneratorMixin, Generator):
@@ -28,6 +29,40 @@ class NullRecipient(RecipientGeneratorMixin, Generator):
     def reference_receivers(self, record=None, request_type=None, **kwargs):
         return None
 
+class FailingGenerator(Generator):
+    def needs(self, **context):
+        raise ValueError("Failing generator")
+
+    def excludes(self, **context):
+        raise ValueError("Failing generator")
+
+    def query_filter(self, **context):
+        raise ValueError("Failing generator")
+
+class R(WorkflowRequestPolicy):
+    req = WorkflowRequest(
+        requesters=[RecordOwners()],
+        recipients=[NullRecipient(), TestRecipient()],
+        transitions=WorkflowTransitions(
+            submitted="pending",
+            accepted="accepted",
+            declined="declined",
+        ),
+    )
+    req1 = WorkflowRequest(
+        requesters=[
+            UserWithRole("administrator"),
+        ],
+        recipients=[NullRecipient(), TestRecipient()],
+    )
+    req2 = WorkflowRequest(
+        requesters = [],    # never applicable, must be created by, for example, system identity
+        recipients = []
+    )
+    req3 = WorkflowRequest(
+        requesters = [FailingGenerator()],
+        recipients = [NullRecipient(), TestRecipient()],
+    )
 
 def test_workflow_requests(users, logged_client, search_clear, record_service):
     req = WorkflowRequest(
@@ -37,6 +72,19 @@ def test_workflow_requests(users, logged_client, search_clear, record_service):
     rec = ThesisRecord.create({})
     assert req.recipient_entity_reference(record=rec) == {"user": "1"}
 
+def test_workflow_requests_no_recipient(users, logged_client, search_clear, record_service):
+    req1 = WorkflowRequest(
+        requesters=[RecordOwners()],
+        recipients=[NullRecipient()],
+    )
+    rec = ThesisRecord.create({})
+    assert req1.recipient_entity_reference(record=rec) is None
+
+    req2 = WorkflowRequest(
+        requesters=[RecordOwners()],
+        recipients=[],
+    )
+    assert req2.recipient_entity_reference(record=rec) is None
 
 def test_request_policy_access(app):
     request_policy = app.config["WORKFLOWS"]["my_workflow"].requests()
@@ -61,17 +109,7 @@ def test_is_applicable(users, logged_client, search_clear, record_service):
 
 
 def test_list_applicable_requests(users, logged_client, search_clear, record_service):
-    class R(WorkflowRequestPolicy):
-        req = WorkflowRequest(
-            requesters=[RecordOwners()],
-            recipients=[NullRecipient(), TestRecipient()],
-        )
-        req1 = WorkflowRequest(
-            requesters=[
-                UserWithRole("administrator"),
-            ],
-            recipients=[NullRecipient(), TestRecipient()],
-        )
+
 
     requests = R()
 
@@ -91,3 +129,30 @@ class R(WorkflowRequestPolicy):
     assert set(
         x[0] for x in requests.applicable_workflow_requests(id2, record=record)
     ) == {"req1"}
+
+
+def test_get_request_type(users, logged_client, search_clear, record_service):
+    requests = R()
+    assert requests["req"] == requests.req
+    assert requests["req1"] == requests.req1
+    with pytest.raises(KeyError):
+        requests["non_existing_request"]        # noqa
+
+def test_transition_getter(users, logged_client, search_clear, record_service):
+    requests = R()
+    assert requests.req.transitions['submitted'] == 'pending'
+    assert requests.req.transitions['accepted'] == 'accepted'
+    assert requests.req.transitions['declined'] == 'declined'
+    with pytest.raises(KeyError):
+        requests.req.transitions['non_existing_transition']        # noqa
+
+
+def test_requestor_filter(users, logged_client, search_clear, record_service):
+    requests = R()
+    sample_record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
+
+    id1 = Identity(id=1)
+    id1.provides.add(UserNeed(1))
+
+    generator = requests.req.requester_generator
+    assert generator.query_filter(identity=id1, record=sample_record) == [Terms(parent__owners__user=[1])]
\ No newline at end of file

From 27ecdd884b84f026882e9e2dc582865cdc264495 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Mon, 18 Nov 2024 11:52:52 +0100
Subject: [PATCH 10/21] coverage settings

---
 .coveragerc | 10 +++++++++-
 1 file changed, 9 insertions(+), 1 deletion(-)

diff --git a/.coveragerc b/.coveragerc
index 1bd1b83..ba18d3c 100644
--- a/.coveragerc
+++ b/.coveragerc
@@ -1,4 +1,12 @@
 [report]
 exclude_lines =
     pragma: no cover
-    if TYPE_CHECKING:
\ No newline at end of file
+    ^#
+exclude_also =
+    if TYPE_CHECKING:
+    if __name__ == .__main__.:
+    if TYPE_CHECKING:
+    class .*\bProtocol\):
+    @(abc\.)?abstractmethod
+    raise AssertionError
+    raise NotImplementedError
\ No newline at end of file

From 8d39706422d0ff632ed0c39447aae5717d2d0490 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Mon, 18 Nov 2024 11:55:48 +0100
Subject: [PATCH 11/21] Sources re-formatted

---
 oarepo_workflows/requests/generators.py       | 28 ++++++++------
 oarepo_workflows/requests/requests.py         |  1 +
 .../services/components/workflow.py           | 14 ++++---
 .../services/permissions/generators.py        |  4 +-
 tests/test_auto_approve.py                    |  7 ++++
 tests/test_auto_request.py                    |  9 ++++-
 tests/test_if_in_state.py                     |  7 ++++
 tests/test_workflow.py                        | 13 ++++---
 tests/test_workflow_permission.py             |  8 ++++
 tests/test_workflow_requests.py               | 38 ++++++++++++-------
 10 files changed, 91 insertions(+), 38 deletions(-)

diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators.py
index 51633cf..444f036 100644
--- a/oarepo_workflows/requests/generators.py
+++ b/oarepo_workflows/requests/generators.py
@@ -59,7 +59,7 @@ def query_filter(self, **context: Any) -> list[dict]:
 
         :param context: Context.
         """
-        ret = []
+        ret: list[dict] = []
         for generator in self.generators:
             query_filter = generator.query_filter(**context)
             if query_filter:
@@ -94,7 +94,7 @@ def reference_receivers(
         record: Optional[Record] = None,
         request_type: Optional[RequestType] = None,
         **context: Any,
-    ) -> list[dict[str, str]]: # pragma: no cover
+    ) -> list[dict[str, str]]:  # pragma: no cover
         """Return the reference receiver(s) of the request.
 
         This call requires the context to contain at least "record" and "request_type"
@@ -126,17 +126,23 @@ def reference_receivers(
         """
         return [{"auto_approve": "True"}]
 
-    def needs(self, **kwargs):
+    def needs(self, **context: Any) -> list[Need]:
         """Get needs that signal workflow to automatically approve the request."""
-        raise ValueError("Auto-approve generator can not create needs and "
-                         "should be used only in `recipient` section of WorkflowRequest.")
+        raise ValueError(
+            "Auto-approve generator can not create needs and "
+            "should be used only in `recipient` section of WorkflowRequest."
+        )
 
-    def excludes(self, **kwargs):
+    def excludes(self, **context: Any) -> list[Need]:
         """Get needs that signal workflow to automatically approve the request."""
-        raise ValueError("Auto-approve generator can not create needs and "
-                         "should be used only in `recipient` section of WorkflowRequest.")
+        raise ValueError(
+            "Auto-approve generator can not create needs and "
+            "should be used only in `recipient` section of WorkflowRequest."
+        )
 
-    def query_filter(self, **kwargs):
+    def query_filter(self, **context: Any) -> list[dict]:
         """Get needs that signal workflow to automatically approve the request."""
-        raise ValueError("Auto-approve generator can not create needs and "
-                         "should be used only in `recipient` section of WorkflowRequest.")
\ No newline at end of file
+        raise ValueError(
+            "Auto-approve generator can not create needs and "
+            "should be used only in `recipient` section of WorkflowRequest."
+        )
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index c006798..cc1d973 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -115,6 +115,7 @@ def __getitem__(self, transition_name: str):
             )
         return getattr(self, transition_name)
 
+
 @dataclasses.dataclass
 class WorkflowRequestEscalation:
     """Escalation of the request.
diff --git a/oarepo_workflows/services/components/workflow.py b/oarepo_workflows/services/components/workflow.py
index d1dedee..c416353 100644
--- a/oarepo_workflows/services/components/workflow.py
+++ b/oarepo_workflows/services/components/workflow.py
@@ -38,15 +38,19 @@ def create(
         """Implement record creation checks and set the workflow on the created record."""
         if not data:
             # sanity check, should be handled by policy before the component is called
-            raise MissingWorkflowError("Workflow not defined in input. As this should be handled by a policy, "
-                                       "make sure you are using workflow-enabled policy.", record=data)   # pragma: no cover
+            raise MissingWorkflowError(
+                "Workflow not defined in input. As this should be handled by a policy, "
+                "make sure you are using workflow-enabled policy.",
+                record=data,
+            )  # pragma: no cover
         try:
             workflow_id = data["parent"]["workflow"]
-        except KeyError as e:               # pragma: no cover
+        except KeyError as e:  # pragma: no cover
             # sanity check, should be handled by policy before the component is called
-            raise MissingWorkflowError(     # pragma: no cover
+            raise MissingWorkflowError(  # pragma: no cover
                 "Workflow not defined in input. As this should be handled by a policy, "
-                "make sure you are using workflow-enabled policy.", record=data
+                "make sure you are using workflow-enabled policy.",
+                record=data,
             ) from e
 
         current_oarepo_workflows.set_workflow(
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 028c38e..9945e4f 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -108,8 +108,8 @@ def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
     def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
         """Return query filters that are generated by the workflow permission.
 
-           Note: this implementation in fact will be called from WorkflowRecordPermissionPolicy.query_filters
-           for each registered workflow type. The query_filters are then combined into a single query.
+        Note: this implementation in fact will be called from WorkflowRecordPermissionPolicy.query_filters
+        for each registered workflow type. The query_filters are then combined into a single query.
         """
         return self._get_permissions_from_workflow(
             record, self._action, **context
diff --git a/tests/test_auto_approve.py b/tests/test_auto_approve.py
index 645ad87..9acbd2a 100644
--- a/tests/test_auto_approve.py
+++ b/tests/test_auto_approve.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from oarepo_workflows.requests.generators import AutoApprove, auto_approve_need
 import pytest
 from invenio_requests.resolvers.registry import ResolverRegistry
diff --git a/tests/test_auto_request.py b/tests/test_auto_request.py
index c4a0e00..be7a57d 100644
--- a/tests/test_auto_request.py
+++ b/tests/test_auto_request.py
@@ -1,6 +1,13 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from oarepo_workflows import AutoRequest
 from oarepo_workflows.requests.generators import auto_request_need
 
 
 def test_auto_request_needs(app):
-    assert AutoRequest().needs() == [auto_request_need]
\ No newline at end of file
+    assert AutoRequest().needs() == [auto_request_need]
diff --git a/tests/test_if_in_state.py b/tests/test_if_in_state.py
index e69de29..892225a 100644
--- a/tests/test_if_in_state.py
+++ b/tests/test_if_in_state.py
@@ -0,0 +1,7 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 7fb5d61..16ab497 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -12,15 +12,18 @@
 from thesis.thesis.records.api import ThesisDraft, ThesisRecord
 
 
-def test_create_without_workflow(users, logged_client, default_workflow_json, search_clear):
+def test_create_without_workflow(
+    users, logged_client, default_workflow_json, search_clear
+):
     # create draft
     user_client1 = logged_client(users[0])
 
-    create_response = user_client1.post(
-        ThesisResourceConfig.url_prefix, json={}
-    )
+    create_response = user_client1.post(ThesisResourceConfig.url_prefix, json={})
     assert create_response.status_code == 400
-    assert create_response.json["errors"][0]["messages"] == ['Workflow not defined in input.']
+    assert create_response.json["errors"][0]["messages"] == [
+        "Workflow not defined in input."
+    ]
+
 
 def test_workflow_read(users, logged_client, default_workflow_json, search_clear):
     # create draft
diff --git a/tests/test_workflow_permission.py b/tests/test_workflow_permission.py
index 41cb5d8..fa5a28c 100644
--- a/tests/test_workflow_permission.py
+++ b/tests/test_workflow_permission.py
@@ -1,3 +1,10 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
 from types import SimpleNamespace
 
 from oarepo_workflows import WorkflowPermission
@@ -18,6 +25,7 @@ def test_get_workflow_id(users, logged_client, search_clear, record_service):
     with pytest.raises(MissingWorkflowError):
         assert wp._get_workflow_id(record=fake_thesis)  # noqa
 
+
 def test_query_filter(users, logged_client, search_clear, record_service):
     wp = WorkflowPermission("read")
 
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index 1579cb2..b13b23a 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -29,6 +29,7 @@ class NullRecipient(RecipientGeneratorMixin, Generator):
     def reference_receivers(self, record=None, request_type=None, **kwargs):
         return None
 
+
 class FailingGenerator(Generator):
     def needs(self, **context):
         raise ValueError("Failing generator")
@@ -39,6 +40,7 @@ def excludes(self, **context):
     def query_filter(self, **context):
         raise ValueError("Failing generator")
 
+
 class R(WorkflowRequestPolicy):
     req = WorkflowRequest(
         requesters=[RecordOwners()],
@@ -56,14 +58,15 @@ class R(WorkflowRequestPolicy):
         recipients=[NullRecipient(), TestRecipient()],
     )
     req2 = WorkflowRequest(
-        requesters = [],    # never applicable, must be created by, for example, system identity
-        recipients = []
+        requesters=[],  # never applicable, must be created by, for example, system identity
+        recipients=[],
     )
     req3 = WorkflowRequest(
-        requesters = [FailingGenerator()],
-        recipients = [NullRecipient(), TestRecipient()],
+        requesters=[FailingGenerator()],
+        recipients=[NullRecipient(), TestRecipient()],
     )
 
+
 def test_workflow_requests(users, logged_client, search_clear, record_service):
     req = WorkflowRequest(
         requesters=[RecordOwners()],
@@ -72,7 +75,10 @@ def test_workflow_requests(users, logged_client, search_clear, record_service):
     rec = ThesisRecord.create({})
     assert req.recipient_entity_reference(record=rec) == {"user": "1"}
 
-def test_workflow_requests_no_recipient(users, logged_client, search_clear, record_service):
+
+def test_workflow_requests_no_recipient(
+    users, logged_client, search_clear, record_service
+):
     req1 = WorkflowRequest(
         requesters=[RecordOwners()],
         recipients=[NullRecipient()],
@@ -86,6 +92,7 @@ def test_workflow_requests_no_recipient(users, logged_client, search_clear, reco
     )
     assert req2.recipient_entity_reference(record=rec) is None
 
+
 def test_request_policy_access(app):
     request_policy = app.config["WORKFLOWS"]["my_workflow"].requests()
     assert getattr(request_policy, "delete_request", None)
@@ -109,8 +116,6 @@ def test_is_applicable(users, logged_client, search_clear, record_service):
 
 
 def test_list_applicable_requests(users, logged_client, search_clear, record_service):
-
-
     requests = R()
 
     id1 = Identity(id=1)
@@ -136,23 +141,28 @@ def test_get_request_type(users, logged_client, search_clear, record_service):
     assert requests["req"] == requests.req
     assert requests["req1"] == requests.req1
     with pytest.raises(KeyError):
-        requests["non_existing_request"]        # noqa
+        requests["non_existing_request"]  # noqa
+
 
 def test_transition_getter(users, logged_client, search_clear, record_service):
     requests = R()
-    assert requests.req.transitions['submitted'] == 'pending'
-    assert requests.req.transitions['accepted'] == 'accepted'
-    assert requests.req.transitions['declined'] == 'declined'
+    assert requests.req.transitions["submitted"] == "pending"
+    assert requests.req.transitions["accepted"] == "accepted"
+    assert requests.req.transitions["declined"] == "declined"
     with pytest.raises(KeyError):
-        requests.req.transitions['non_existing_transition']        # noqa
+        requests.req.transitions["non_existing_transition"]  # noqa
 
 
 def test_requestor_filter(users, logged_client, search_clear, record_service):
     requests = R()
-    sample_record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
+    sample_record = SimpleNamespace(
+        parent=SimpleNamespace(owners=[SimpleNamespace(id=1)])
+    )
 
     id1 = Identity(id=1)
     id1.provides.add(UserNeed(1))
 
     generator = requests.req.requester_generator
-    assert generator.query_filter(identity=id1, record=sample_record) == [Terms(parent__owners__user=[1])]
\ No newline at end of file
+    assert generator.query_filter(identity=id1, record=sample_record) == [
+        Terms(parent__owners__user=[1])
+    ]

From f525f7810136e3ec162b24fbd0da6fcc86386889 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Wed, 20 Nov 2024 11:22:22 +0100
Subject: [PATCH 12/21] Renaming etc.

---
 oarepo_workflows/__init__.py                  |  2 +
 oarepo_workflows/ext.py                       | 10 +++--
 oarepo_workflows/proxies.py                   |  7 +++-
 oarepo_workflows/requests/policy.py           |  6 +--
 oarepo_workflows/requests/requests.py         |  1 +
 .../resolvers/auto_approve/__init__.py        |  2 +-
 .../services/permissions/__init__.py          |  3 +-
 .../services/permissions/generators.py        | 25 ++++++++---
 .../permissions/record_permission_policy.py   | 42 +++++++++----------
 .../permissions/workflow_permissions.py       |  2 +-
 oarepo_workflows/views/api.py                 |  5 ++-
 oarepo_workflows/views/app.py                 |  5 ++-
 tests/test_workflow_permission.py             |  6 +--
 13 files changed, 72 insertions(+), 44 deletions(-)

diff --git a/oarepo_workflows/__init__.py b/oarepo_workflows/__init__.py
index 0621c00..56f1a53 100644
--- a/oarepo_workflows/__init__.py
+++ b/oarepo_workflows/__init__.py
@@ -12,6 +12,7 @@
 from oarepo_workflows.services.permissions import (
     IfInState,
     WorkflowPermission,
+    FromRecordWorkflow,
     WorkflowRecordPermissionPolicy,
 )
 
@@ -36,4 +37,5 @@
     "WorkflowRequest",
     "WorkflowRequestEscalation",
     "WorkflowTransitions",
+    "FromRecordWorkflow",
 )
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index d7c7eae..8c15019 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -52,6 +52,7 @@ def __init__(self, app: Optional[Flask] = None) -> None:
             self.init_app(app)
             self.init_services()
 
+    # noinspection PyMethodMayBeStatic
     def init_config(self, app: Flask) -> None:
         """Initialize configuration.
 
@@ -72,7 +73,8 @@ def init_config(self, app: Flask) -> None:
 
     def init_services(self) -> None:
         """Initialize workflow services."""
-        self.autoapprove_service = AutoApproveEntityService(
+        # noinspection PyAttributeOutsideInit
+        self.auto_approve_service = AutoApproveEntityService(
             config=AutoApproveEntityServiceConfig()
         )
 
@@ -178,6 +180,7 @@ def set_workflow(
                 **kwargs,
             )
 
+    # noinspection PyMethodMayBeStatic
     def get_workflow_from_record(self, record: Record | ParentRecord) -> str | None:
         """Get the workflow from a record.
 
@@ -224,6 +227,7 @@ def get_workflow(self, record: Record) -> Workflow:
 
     def init_app(self, app: Flask) -> None:
         """Flask application initialization."""
+        # noinspection PyAttributeOutsideInit
         self.app = app
         app.extensions["oarepo-workflows"] = self
 
@@ -241,6 +245,6 @@ def finalize_app(app: Flask) -> None:
     ext = app.extensions["oarepo-workflows"]
 
     records_resources.registry.register(
-        ext.autoapprove_service,
-        service_id=ext.autoapprove_service.config.service_id,
+        ext.auto_approve_service,
+        service_id=ext.auto_approve_service.config.service_id,
     )
diff --git a/oarepo_workflows/proxies.py b/oarepo_workflows/proxies.py
index 58c803b..fcbc295 100644
--- a/oarepo_workflows/proxies.py
+++ b/oarepo_workflows/proxies.py
@@ -9,10 +9,15 @@
 
 from __future__ import annotations
 
+from typing import TYPE_CHECKING
+
 from flask import current_app
 from werkzeug.local import LocalProxy
 
-current_oarepo_workflows = LocalProxy(
+if TYPE_CHECKING:
+    from oarepo_workflows.ext import OARepoWorkflows
+
+current_oarepo_workflows: OARepoWorkflows = LocalProxy(     # type: ignore
     lambda: current_app.extensions["oarepo-workflows"]
 )
 """Proxy to access the current OARepo workflows extension."""
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index b628a6c..12f345b 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -9,7 +9,6 @@
 
 from __future__ import annotations
 
-from functools import cached_property
 from typing import TYPE_CHECKING, Any
 
 from .requests import (
@@ -54,7 +53,6 @@ def __getitem__(self, request_type_id: str) -> WorkflowRequest:
                 f"Request type {request_type_id} not defined in {self.__class__.__name__}"
             ) from None
 
-    @cached_property
     def items(self) -> list[tuple[str, WorkflowRequest]]:
         """Return the list of request types and their instances.
 
@@ -79,11 +77,11 @@ def applicable_workflow_requests(
 
         :param identity: Identity of the requester.
         :param context: Context of the request that is passed to the requester generators.
-        :return List of tuples (request_type_id, request) that are applicable for the identity and context.
+        :return: List of tuples (request_type_id, request) that are applicable for the identity and context.
         """
         ret = []
 
-        for name, request in self.items:
+        for name, request in self.items():
             if request.is_applicable(identity, **context):
                 ret.append((name, request))
         return ret
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index cc1d973..83c1ab4 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -129,6 +129,7 @@ class WorkflowRequestEscalation:
     recipients: list[Generator] | tuple[Generator]
 
 
+# noinspection PyPep8Naming
 def RecipientEntityReference(request: WorkflowRequest, **context: Any) -> dict | None:
     """Return the reference receiver of the workflow request with the given context.
 
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index 683b8f9..e88d16a 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -31,7 +31,7 @@ def _resolve(self) -> AutoApproveEntity:
 
     def get_needs(self, ctx: dict | None = None) -> list[Need]:
         """Get needs that the entity generate."""
-        return []  # granttokens calls this
+        return []  # grant_tokens calls this
 
     def pick_resolved_fields(self, identity: Identity, resolved_dict: dict) -> dict:
         """Pick resolved fields for serialization of the entity to json."""
diff --git a/oarepo_workflows/services/permissions/__init__.py b/oarepo_workflows/services/permissions/__init__.py
index efb3e97..5c157b0 100644
--- a/oarepo_workflows/services/permissions/__init__.py
+++ b/oarepo_workflows/services/permissions/__init__.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from .generators import IfInState, WorkflowPermission
+from .generators import IfInState, WorkflowPermission, FromRecordWorkflow
 from .record_permission_policy import WorkflowRecordPermissionPolicy
 from .workflow_permissions import DefaultWorkflowPermissions
 
@@ -18,4 +18,5 @@
     "WorkflowPermission",
     "WorkflowRecordPermissionPolicy",
     "DefaultWorkflowPermissions",
+    "FromRecordWorkflow",
 )
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 9945e4f..d7fd30c 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -33,7 +33,7 @@ def _make_query(generators: Iterable[Generator], **context: Any) -> dict | None:
     return reduce(operator.or_, queries) if queries else None
 
 
-class WorkflowPermission(Generator):
+class FromRecordWorkflow(Generator):
     """Permission delegating check to workflow.
 
     The implementation of the permission gets the workflow id from the passed context
@@ -43,12 +43,15 @@ class WorkflowPermission(Generator):
     determine the permissions for the action.
     """
 
-    def __init__(self, action: str | None = None) -> None:
+    _action: str
+
+    def __init__(self, action: str) -> None:
         """Initialize the permission."""
         # might not be needed in subclasses
         super().__init__()
         self._action = action
 
+    # noinspection PyMethodMayBeStatic
     def _get_workflow_id(self, record: Optional[Record] = None, **context: Any) -> str:
         """Get the workflow id from the context.
 
@@ -79,8 +82,8 @@ def _get_workflow_id(self, record: Optional[Record] = None, **context: Any) -> s
 
     def _get_permissions_from_workflow(
         self,
+        action_name: str,
         record: Optional[Record] = None,
-        action_name: str | None = None,
         **context: Any,
     ) -> RecordPermissionPolicy:
         """Get the permissions policy from the workflow.
@@ -102,9 +105,15 @@ def _get_permissions_from_workflow(
     def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
         """Return needs that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(
-            record, self._action, **context
+            self._action, record, **context
         ).needs
 
+    def excludes(self, **context: Any) -> set[Need]:
+        """Return excludes that are generated by the workflow permission."""
+        return self._get_permissions_from_workflow(
+            self._action, **context
+        ).excludes
+
     def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
         """Return query filters that are generated by the workflow permission.
 
@@ -112,9 +121,14 @@ def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
         for each registered workflow type. The query_filters are then combined into a single query.
         """
         return self._get_permissions_from_workflow(
-            record, self._action, **context
+            self._action, record, **context
         ).query_filters
 
+class WorkflowPermission(FromRecordWorkflow):
+    def __init__(self, action: str) -> None:
+        import warnings
+        warnings.warn("WorkflowPermission is deprecated. Use FromRecordWorkflow instead.", DeprecationWarning)
+        super().__init__(action)
 
 class IfInState(ConditionalGenerator):
     """Generator that checks if the record is in a specific state.
@@ -186,6 +200,7 @@ def __init__(self, permission_name: str) -> None:
         """
         self.delegated_permission_name = permission_name
 
+    # noinspection PyUnusedLocal
     def _generators(
         self, *, policy: RecordPermissionPolicy, **context: Any
     ) -> list[Generator]:
diff --git a/oarepo_workflows/services/permissions/record_permission_policy.py b/oarepo_workflows/services/permissions/record_permission_policy.py
index 8d1ee24..d23d7ed 100644
--- a/oarepo_workflows/services/permissions/record_permission_policy.py
+++ b/oarepo_workflows/services/permissions/record_permission_policy.py
@@ -19,7 +19,7 @@
 from invenio_search.engine import dsl
 
 from ...proxies import current_oarepo_workflows
-from .generators import WorkflowPermission
+from .generators import FromRecordWorkflow
 
 
 class WorkflowRecordPermissionPolicy(RecordPermissionPolicy):
@@ -28,25 +28,25 @@ class WorkflowRecordPermissionPolicy(RecordPermissionPolicy):
     Do not use this class in Workflow constructor.
     """
 
-    can_create = [WorkflowPermission("create")]
-    can_publish = [WorkflowPermission("publish")]
-    can_read = [WorkflowPermission("read")]
-    can_update = [WorkflowPermission("update")]
-    can_delete = [WorkflowPermission("delete")]
-    can_create_files = [WorkflowPermission("create_files")]
-    can_set_content_files = [WorkflowPermission("set_content_files")]
-    can_get_content_files = [WorkflowPermission("get_content_files")]
-    can_commit_files = [WorkflowPermission("commit_files")]
-    can_read_files = [WorkflowPermission("read_files")]
-    can_update_files = [WorkflowPermission("update_files")]
-    can_delete_files = [WorkflowPermission("delete_files")]
+    can_create = [FromRecordWorkflow("create")]
+    can_publish = [FromRecordWorkflow("publish")]
+    can_read = [FromRecordWorkflow("read")]
+    can_update = [FromRecordWorkflow("update")]
+    can_delete = [FromRecordWorkflow("delete")]
+    can_create_files = [FromRecordWorkflow("create_files")]
+    can_set_content_files = [FromRecordWorkflow("set_content_files")]
+    can_get_content_files = [FromRecordWorkflow("get_content_files")]
+    can_commit_files = [FromRecordWorkflow("commit_files")]
+    can_read_files = [FromRecordWorkflow("read_files")]
+    can_update_files = [FromRecordWorkflow("update_files")]
+    can_delete_files = [FromRecordWorkflow("delete_files")]
 
-    can_read_draft = [WorkflowPermission("read_draft")]
-    can_update_draft = [WorkflowPermission("update_draft")]
-    can_delete_draft = [WorkflowPermission("delete_draft")]
-    can_edit = [WorkflowPermission("edit")]
-    can_new_version = [WorkflowPermission("new_version")]
-    can_draft_create_files = [WorkflowPermission("draft_create_files")]
+    can_read_draft = [FromRecordWorkflow("read_draft")]
+    can_update_draft = [FromRecordWorkflow("update_draft")]
+    can_delete_draft = [FromRecordWorkflow("delete_draft")]
+    can_edit = [FromRecordWorkflow("edit")]
+    can_new_version = [FromRecordWorkflow("new_version")]
+    can_draft_create_files = [FromRecordWorkflow("draft_create_files")]
 
     can_search = [SystemProcess(), AnyUser()]
     can_search_drafts = [SystemProcess(), AnyUser()]
@@ -60,12 +60,12 @@ def query_filters(self) -> list[dict]:
         workflows = current_oarepo_workflows.record_workflows
         queries = []
         for workflow_id, workflow in workflows.items():
-            q_inworkflow = dsl.Q("term", **{"parent.workflow": workflow_id})
+            q_in_workflow = dsl.Q("term", **{"parent.workflow": workflow_id})
             workflow_filters = workflow.permissions(
                 self.action, **self.over
             ).query_filters
             if not workflow_filters:
                 workflow_filters = [dsl.Q("match_none")]
-            query = reduce(lambda f1, f2: f1 | f2, workflow_filters) & q_inworkflow
+            query = reduce(lambda f1, f2: f1 | f2, workflow_filters) & q_in_workflow
             queries.append(query)
         return [q for q in queries if q]
diff --git a/oarepo_workflows/services/permissions/workflow_permissions.py b/oarepo_workflows/services/permissions/workflow_permissions.py
index 1ec44d9..ffda667 100644
--- a/oarepo_workflows/services/permissions/workflow_permissions.py
+++ b/oarepo_workflows/services/permissions/workflow_permissions.py
@@ -37,7 +37,7 @@ class MyWorkflowPermissions(DefaultWorkflowPermissions):
 
     """
 
-    # new version - update; edit current version - disable -> idk if there's other way than something like IfNoEditDraft/IfNoNewVersionDraft generators-
+    # TODO: new version - update; edit current version - disable -> idk if there's other way than something like IfNoEditDraft/IfNoNewVersionDraft generators-
 
     files_edit = [
         IfInState("draft", [RecordOwners()]),
diff --git a/oarepo_workflows/views/api.py b/oarepo_workflows/views/api.py
index e788ca9..6e61a03 100644
--- a/oarepo_workflows/views/api.py
+++ b/oarepo_workflows/views/api.py
@@ -21,12 +21,13 @@ def create_api_blueprint(app: Flask) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state: BlueprintSetupState) -> None:
+    # noinspection PyUnusedLocal
+    def register_auto_approve_entity_resolver(state: BlueprintSetupState) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
 
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())
 
-    blueprint.record_once(register_autoapprove_entity_resolver)
+    blueprint.record_once(register_auto_approve_entity_resolver)
 
     return blueprint
diff --git a/oarepo_workflows/views/app.py b/oarepo_workflows/views/app.py
index c090bea..751fd2c 100644
--- a/oarepo_workflows/views/app.py
+++ b/oarepo_workflows/views/app.py
@@ -21,12 +21,13 @@ def create_app_blueprint(app: Flask) -> Blueprint:
     """Create requests blueprint."""
     blueprint = Blueprint("oarepo-workflows", __name__)
 
-    def register_autoapprove_entity_resolver(state: BlueprintSetupState) -> None:
+    # noinspection PyUnusedLocal
+    def register_auto_approve_entity_resolver(state: BlueprintSetupState) -> None:
         from oarepo_workflows.resolvers.auto_approve import AutoApproveResolver
 
         requests = app.extensions["invenio-requests"]
         requests.entity_resolvers_registry.register_type(AutoApproveResolver())
 
-    blueprint.record_once(register_autoapprove_entity_resolver)
+    blueprint.record_once(register_auto_approve_entity_resolver)
 
     return blueprint
diff --git a/tests/test_workflow_permission.py b/tests/test_workflow_permission.py
index fa5a28c..11049af 100644
--- a/tests/test_workflow_permission.py
+++ b/tests/test_workflow_permission.py
@@ -7,7 +7,7 @@
 #
 from types import SimpleNamespace
 
-from oarepo_workflows import WorkflowPermission
+from oarepo_workflows import FromRecordWorkflow
 from oarepo_workflows.errors import MissingWorkflowError
 from thesis.thesis.records.api import ThesisRecord
 import pytest
@@ -17,7 +17,7 @@
 
 def test_get_workflow_id(users, logged_client, search_clear, record_service):
     thesis = ThesisRecord.create({})
-    wp = WorkflowPermission("read")
+    wp = FromRecordWorkflow("read")
     with pytest.raises(MissingWorkflowError):
         wp._get_workflow_id(record=thesis)
 
@@ -27,7 +27,7 @@ def test_get_workflow_id(users, logged_client, search_clear, record_service):
 
 
 def test_query_filter(users, logged_client, search_clear, record_service):
-    wp = WorkflowPermission("read")
+    wp = FromRecordWorkflow("read")
 
     id1 = Identity(id=1)
     id1.provides.add(UserNeed(1))

From 9882eb545c6309bde47eb837afb2630da42d496a Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Wed, 20 Nov 2024 14:32:11 +0100
Subject: [PATCH 13/21] Applicable requests  evaluate can_create

---
 oarepo_workflows/__init__.py                  |  2 +-
 oarepo_workflows/errors.py                    |  4 +++
 oarepo_workflows/proxies.py                   |  2 +-
 oarepo_workflows/requests/generators.py       |  2 +-
 oarepo_workflows/requests/policy.py           |  5 ++++
 oarepo_workflows/requests/requests.py         | 29 +++++++++++++++++-
 .../resolvers/auto_approve/__init__.py        |  2 +-
 .../services/permissions/__init__.py          |  2 +-
 .../services/permissions/generators.py        | 24 +++++++++++----
 tests/conftest.py                             | 15 +++++++++-
 tests/test_auto_approve.py                    | 11 ++++---
 tests/test_workflow.py                        |  2 +-
 tests/test_workflow_permission.py             |  2 +-
 tests/test_workflow_requests.py               | 30 +++++++++++++++----
 tests/utils.py                                |  4 +--
 15 files changed, 107 insertions(+), 29 deletions(-)

diff --git a/oarepo_workflows/__init__.py b/oarepo_workflows/__init__.py
index 56f1a53..7f36821 100644
--- a/oarepo_workflows/__init__.py
+++ b/oarepo_workflows/__init__.py
@@ -10,9 +10,9 @@
 from __future__ import annotations
 
 from oarepo_workflows.services.permissions import (
+    FromRecordWorkflow,
     IfInState,
     WorkflowPermission,
-    FromRecordWorkflow,
     WorkflowRecordPermissionPolicy,
 )
 
diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index d01643f..8de5452 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -72,3 +72,7 @@ def __init__(
             super().__init__(f"{message} Used on community {community_id}")
         else:
             super().__init__(message)
+
+
+class InvalidConfigurationError(Exception):
+    """Exception raised when a configuration is invalid."""
diff --git a/oarepo_workflows/proxies.py b/oarepo_workflows/proxies.py
index fcbc295..221147f 100644
--- a/oarepo_workflows/proxies.py
+++ b/oarepo_workflows/proxies.py
@@ -17,7 +17,7 @@
 if TYPE_CHECKING:
     from oarepo_workflows.ext import OARepoWorkflows
 
-current_oarepo_workflows: OARepoWorkflows = LocalProxy(     # type: ignore
+current_oarepo_workflows: OARepoWorkflows = LocalProxy(  # type: ignore
     lambda: current_app.extensions["oarepo-workflows"]
 )
 """Proxy to access the current OARepo workflows extension."""
diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators.py
index 444f036..b1b57d6 100644
--- a/oarepo_workflows/requests/generators.py
+++ b/oarepo_workflows/requests/generators.py
@@ -124,7 +124,7 @@ def reference_receivers(
 
         Returning "auto_approve" is a signal to the workflow that the request should be auto-approved.
         """
-        return [{"auto_approve": "True"}]
+        return [{"auto_approve": "true"}]
 
     def needs(self, **context: Any) -> list[Need]:
         """Get needs that signal workflow to automatically approve the request."""
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index 12f345b..4b539cf 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -44,6 +44,11 @@ class MyWorkflowRequests(WorkflowRequestPolicy):
 
     """
 
+    def __init__(self):
+        """Initialize the request policy."""
+        for rt_code, rt in self.items():
+            rt._request_type = rt_code
+
     def __getitem__(self, request_type_id: str) -> WorkflowRequest:
         """Get the workflow request type by its id."""
         try:
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index 83c1ab4..de005bc 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -15,15 +15,18 @@
 from typing import TYPE_CHECKING, Any, Optional
 
 from flask_principal import Identity, Permission
+from invenio_requests.proxies import current_request_type_registry
 
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.requests import RecipientGeneratorMixin
 from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
+from oarepo_workflows.errors import InvalidConfigurationError
 
 if TYPE_CHECKING:
     from datetime import timedelta
 
     from invenio_records_permissions.generators import Generator
+    from invenio_requests.customizations.request_types import RequestType
 
     from oarepo_workflows.requests.events import WorkflowEvent
 
@@ -56,6 +59,8 @@ class WorkflowRequest:
     escalations: Optional[list[WorkflowRequestEscalation]] = None
     """Escalations applied to the request if not approved/declined in time."""
 
+    _request_type: RequestType | None = dataclasses.field(default=None, init=False)
+
     @cached_property
     def requester_generator(self) -> Generator:
         """Return the requesters as a single requester generator."""
@@ -82,11 +87,33 @@ def is_applicable(self, identity: Identity, **context: Any) -> bool:
             if not p.needs:
                 return False
             p.excludes.update(self.requester_generator.excludes(**context))
-            return p.allows(identity)
+            if not p.allows(identity):
+                return False
+            if hasattr(self.request_type, "can_create"):
+                return self.request_type.can_create(identity, **context)
+            return True
+        except InvalidConfigurationError:
+            raise
         except Exception as e:
             log.exception("Error checking request applicability: %s", e)
             return False
 
+    @cached_property
+    def request_type(self) -> type[RequestType]:
+        """Return the request type for the workflow request."""
+        if self._request_type is None:
+            raise InvalidConfigurationError(
+                f"Probably this WorkflowRequest ({self}) is not part of a Workflow. "
+                "Please add it to a workflow or manually set the ._request_type attribute "
+                "to a code of a registered request type."
+            )
+        try:
+            return current_request_type_registry.lookup(self._request_type)
+        except KeyError as e:
+            raise InvalidConfigurationError(
+                f"Request type {self._request_type} not found in the request type registry."
+            ) from e
+
     @property
     def allowed_events(self) -> dict[str, WorkflowEvent]:
         """Return the allowed events for the workflow request."""
diff --git a/oarepo_workflows/resolvers/auto_approve/__init__.py b/oarepo_workflows/resolvers/auto_approve/__init__.py
index e88d16a..f13c90c 100644
--- a/oarepo_workflows/resolvers/auto_approve/__init__.py
+++ b/oarepo_workflows/resolvers/auto_approve/__init__.py
@@ -53,7 +53,7 @@ def matches_reference_dict(self, ref_dict: dict) -> bool:
 
     def _reference_entity(self, entity: Any) -> dict[str, str]:
         """Return a reference dictionary for the entity."""
-        return {self.type_id: str(True)}
+        return {self.type_id: "true"}
 
     def matches_entity(self, entity: Any) -> bool:
         """Check if the entity can be serialized to a reference by this resolver."""
diff --git a/oarepo_workflows/services/permissions/__init__.py b/oarepo_workflows/services/permissions/__init__.py
index 5c157b0..7aba29a 100644
--- a/oarepo_workflows/services/permissions/__init__.py
+++ b/oarepo_workflows/services/permissions/__init__.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from .generators import IfInState, WorkflowPermission, FromRecordWorkflow
+from .generators import FromRecordWorkflow, IfInState, WorkflowPermission
 from .record_permission_policy import WorkflowRecordPermissionPolicy
 from .workflow_permissions import DefaultWorkflowPermissions
 
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index d7fd30c..48809dd 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -110,9 +110,7 @@ def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
 
     def excludes(self, **context: Any) -> set[Need]:
         """Return excludes that are generated by the workflow permission."""
-        return self._get_permissions_from_workflow(
-            self._action, **context
-        ).excludes
+        return self._get_permissions_from_workflow(self._action, **context).excludes
 
     def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
         """Return query filters that are generated by the workflow permission.
@@ -124,12 +122,22 @@ def query_filter(self, record: Optional[Record] = None, **context: Any) -> dict:
             self._action, record, **context
         ).query_filters
 
+
 class WorkflowPermission(FromRecordWorkflow):
+    """Deprecated alias for FromRecordWorkflow."""
+
     def __init__(self, action: str) -> None:
+        """Initialize the generator."""
         import warnings
-        warnings.warn("WorkflowPermission is deprecated. Use FromRecordWorkflow instead.", DeprecationWarning)
+
+        warnings.warn(
+            "WorkflowPermission is deprecated. Use FromRecordWorkflow instead.",
+            DeprecationWarning,
+            stacklevel=2,
+        )
         super().__init__(action)
 
+
 class IfInState(ConditionalGenerator):
     """Generator that checks if the record is in a specific state.
 
@@ -145,10 +153,14 @@ class IfInState(ConditionalGenerator):
     def __init__(
         self,
         state: str,
-        then_: list[Generator] | tuple[Generator] | None = None,
-        else_: list[Generator] | tuple[Generator] | None = None,
+        then_: list[Generator] | tuple[Generator] | Generator | None = None,
+        else_: list[Generator] | tuple[Generator] | Generator | None = None,
     ) -> None:
         """Initialize the generator."""
+        if isinstance(then_, Generator):
+            then_ = [then_]
+        if isinstance(else_, Generator):
+            else_ = [else_]
         super().__init__(then_ or [], else_=else_ or [])
         self.state = state
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 7e4b2e0..9c78d3e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -16,7 +16,9 @@
 from invenio_i18n import lazy_gettext as _
 from invenio_records_permissions.generators import Generator
 from invenio_users_resources.records import UserAggregate
-from oarepo_runtime.services.generators import RecordOwners
+from invenio_requests.customizations.request_types import RequestType
+from invenio_requests.proxies import current_request_type_registry
+from oarepo_runtime.services.permissions import RecordOwners
 
 from oarepo_workflows.base import Workflow
 from oarepo_workflows.requests import (
@@ -217,3 +219,14 @@ def app_config(app_config):
 @pytest.fixture()
 def default_workflow_json():
     return {"parent": {"workflow": "my_workflow"}}
+
+
+@pytest.fixture()
+def extra_request_types():
+    def create_rt(type_id):
+        return type("Req", (RequestType,), {"type_id": type_id})
+
+    current_request_type_registry.register_type(create_rt("req"), force=True)
+    current_request_type_registry.register_type(create_rt("req1"), force=True)
+    current_request_type_registry.register_type(create_rt("req2"), force=True)
+    current_request_type_registry.register_type(create_rt("req3"), force=True)
diff --git a/tests/test_auto_approve.py b/tests/test_auto_approve.py
index 9acbd2a..c56b132 100644
--- a/tests/test_auto_approve.py
+++ b/tests/test_auto_approve.py
@@ -5,12 +5,11 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
-from oarepo_workflows.requests.generators import AutoApprove, auto_approve_need
+from oarepo_workflows.requests.generators import AutoApprove
 import pytest
 from invenio_requests.resolvers.registry import ResolverRegistry
 
-from oarepo_workflows.resolvers.auto_approve import AutoApproveProxy, AutoApproveEntity
-from invenio_access.permissions import system_identity
+from oarepo_workflows.resolvers.auto_approve import AutoApproveEntity
 
 
 def test_auto_approve_generator():
@@ -25,11 +24,11 @@ def test_auto_approve_generator():
     with pytest.raises(ValueError):
         a.query_filter()
 
-    assert a.reference_receivers() == [{"auto_approve": "True"}]
+    assert a.reference_receivers() == [{"auto_approve": "true"}]
 
 
 def test_auto_approve_resolver(app):
-    resolved = ResolverRegistry.resolve_entity({"auto_approve": "True"})
+    resolved = ResolverRegistry.resolve_entity({"auto_approve": "true"})
     assert isinstance(resolved, AutoApproveEntity)
     #
     # assert resolved.resolve() == AutoApprove()
@@ -37,4 +36,4 @@ def test_auto_approve_resolver(app):
     # assert resolved.get_needs() == []
 
     entity_reference = ResolverRegistry.reference_entity(AutoApproveEntity())
-    assert entity_reference == {"auto_approve": "True"}
+    assert entity_reference == {"auto_approve": "true"}
diff --git a/tests/test_workflow.py b/tests/test_workflow.py
index 16ab497..63957ae 100644
--- a/tests/test_workflow.py
+++ b/tests/test_workflow.py
@@ -9,7 +9,7 @@
 
 from oarepo_workflows.errors import InvalidWorkflowError
 from thesis.resources.records.config import ThesisResourceConfig
-from thesis.thesis.records.api import ThesisDraft, ThesisRecord
+from thesis.records.api import ThesisDraft, ThesisRecord
 
 
 def test_create_without_workflow(
diff --git a/tests/test_workflow_permission.py b/tests/test_workflow_permission.py
index 11049af..1e11322 100644
--- a/tests/test_workflow_permission.py
+++ b/tests/test_workflow_permission.py
@@ -9,7 +9,7 @@
 
 from oarepo_workflows import FromRecordWorkflow
 from oarepo_workflows.errors import MissingWorkflowError
-from thesis.thesis.records.api import ThesisRecord
+from thesis.records.api import ThesisRecord
 import pytest
 
 from flask_principal import Identity, UserNeed
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index b13b23a..d7be03a 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -12,7 +12,7 @@
 
 from oarepo_workflows import WorkflowRequestPolicy, WorkflowTransitions
 from oarepo_workflows.requests import RecipientGeneratorMixin, WorkflowRequest
-from thesis.thesis.records.api import ThesisRecord
+from thesis.records.api import ThesisRecord
 
 from flask_principal import Identity, UserNeed, RoleNeed
 import pytest
@@ -99,11 +99,13 @@ def test_request_policy_access(app):
     assert not getattr(request_policy, "non_existing_request", None)
 
 
-def test_is_applicable(users, logged_client, search_clear, record_service):
+def test_is_applicable(users, logged_client, search_clear, record_service, extra_request_types):
     req = WorkflowRequest(
         requesters=[RecordOwners()],
         recipients=[NullRecipient(), TestRecipient()],
     )
+    req._request_type = "req"
+
     id1 = Identity(id=1)
     id1.provides.add(UserNeed(1))
 
@@ -115,7 +117,9 @@ def test_is_applicable(users, logged_client, search_clear, record_service):
     assert req.is_applicable(id1, record=record) is True
 
 
-def test_list_applicable_requests(users, logged_client, search_clear, record_service):
+def test_list_applicable_requests(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
     requests = R()
 
     id1 = Identity(id=1)
@@ -136,7 +140,9 @@ def test_list_applicable_requests(users, logged_client, search_clear, record_ser
     ) == {"req1"}
 
 
-def test_get_request_type(users, logged_client, search_clear, record_service):
+def test_get_workflow_request_via_index(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
     requests = R()
     assert requests["req"] == requests.req
     assert requests["req1"] == requests.req1
@@ -144,7 +150,17 @@ def test_get_request_type(users, logged_client, search_clear, record_service):
         requests["non_existing_request"]  # noqa
 
 
-def test_transition_getter(users, logged_client, search_clear, record_service):
+def test_get_request_type(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
+    requests = R()
+    for rt_code, wr in requests.items():
+        assert wr.request_type.type_id == rt_code
+
+
+def test_transition_getter(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
     requests = R()
     assert requests.req.transitions["submitted"] == "pending"
     assert requests.req.transitions["accepted"] == "accepted"
@@ -153,7 +169,9 @@ def test_transition_getter(users, logged_client, search_clear, record_service):
         requests.req.transitions["non_existing_transition"]  # noqa
 
 
-def test_requestor_filter(users, logged_client, search_clear, record_service):
+def test_requestor_filter(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
     requests = R()
     sample_record = SimpleNamespace(
         parent=SimpleNamespace(owners=[SimpleNamespace(id=1)])
diff --git a/tests/utils.py b/tests/utils.py
index c3424ad..998a717 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -1,8 +1,8 @@
-def test_state_change_notifier(*args, **kwargs):
+def test_state_change_notifier(*args, **_kwargs):
     record = args[1]
     record["state-change-notifier-called"] = True
 
 
-def test_workflow_change_notifier(*args, **kwargs):
+def test_workflow_change_notifier(*args, **_kwargs):
     record = args[1]
     record.parent["workflow-change-notifier-called"] = True

From 50e0add9e0f304345a733352448caa30bda3fbd4 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Wed, 20 Nov 2024 14:32:39 +0100
Subject: [PATCH 14/21] Formatting

---
 oarepo_workflows/requests/requests.py | 2 +-
 tests/test_workflow_requests.py       | 4 +++-
 2 files changed, 4 insertions(+), 2 deletions(-)

diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index de005bc..d133dea 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -17,10 +17,10 @@
 from flask_principal import Identity, Permission
 from invenio_requests.proxies import current_request_type_registry
 
+from oarepo_workflows.errors import InvalidConfigurationError
 from oarepo_workflows.proxies import current_oarepo_workflows
 from oarepo_workflows.requests import RecipientGeneratorMixin
 from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
-from oarepo_workflows.errors import InvalidConfigurationError
 
 if TYPE_CHECKING:
     from datetime import timedelta
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index d7be03a..5dab92e 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -99,7 +99,9 @@ def test_request_policy_access(app):
     assert not getattr(request_policy, "non_existing_request", None)
 
 
-def test_is_applicable(users, logged_client, search_clear, record_service, extra_request_types):
+def test_is_applicable(
+    users, logged_client, search_clear, record_service, extra_request_types
+):
     req = WorkflowRequest(
         requesters=[RecordOwners()],
         recipients=[NullRecipient(), TestRecipient()],

From 77cdd3f31c3143bf95653aac06648b227b8035b6 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Wed, 20 Nov 2024 14:34:51 +0100
Subject: [PATCH 15/21] Python version

---
 .github/workflows/build.yaml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 3417990..3375403 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -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:

From 85f8872cd032f835e892865837be5917ae914aa6 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Wed, 20 Nov 2024 20:01:04 +0100
Subject: [PATCH 16/21] Extracting workflow even from dict

---
 oarepo_workflows/ext.py               | 31 +++++++++++++++++++++------
 oarepo_workflows/requests/policy.py   |  5 +++--
 oarepo_workflows/requests/requests.py | 13 +++++------
 3 files changed, 35 insertions(+), 14 deletions(-)

diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 8c15019..66f306f 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -14,6 +14,7 @@
 
 import importlib_metadata
 from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
+from invenio_records_resources.records import Record
 from invenio_records_resources.services.uow import RecordCommitOp, unit_of_work
 from oarepo_runtime.datastreams.utils import get_record_service_for_record
 
@@ -28,7 +29,6 @@
     from flask import Flask
     from flask_principal import Identity
     from invenio_drafts_resources.records import ParentRecord
-    from invenio_records_resources.records import Record
     from invenio_records_resources.services.uow import UnitOfWork
 
     from oarepo_workflows.base import (
@@ -207,18 +207,37 @@ def default_workflow_events(self) -> dict:
         """
         return self.app.config.get("DEFAULT_WORKFLOW_EVENTS", {})
 
-    def get_workflow(self, record: Record) -> Workflow:
+    def get_workflow(self, record: Record | dict) -> Workflow:
         """Get the workflow for a record.
 
         :param record:  record to get the workflow for
         :raises MissingWorkflowError: if the workflow is not found
         :raises InvalidWorkflowError: if the workflow is invalid
         """
-        parent = record.parent  # noqa for typing: we do not have a better type for record with parent
+        if isinstance(record, Record):
+            try:
+                parent = record.parent  # noqa for typing: we do not have a better type for record with parent
+            except AttributeError as e:
+                raise MissingWorkflowError("Record does not have a parent attribute, is it a draft-enabled record?",
+                                           record=record) from e
+            try:
+                workflow_id = parent.workflow
+            except AttributeError as e:
+                raise MissingWorkflowError("Parent record does not have a workflow attribute.",
+                                           record=record) from e
+        else:
+            try:
+                parent = record["parent"]
+            except KeyError as e:
+                raise MissingWorkflowError("Record does not have a parent attribute.",
+                                           record=record) from e
+            try:
+                workflow_id = parent["workflow"]
+            except KeyError as e:
+                raise MissingWorkflowError("Parent record does not have a workflow attribute.", record=record) from e
+
         try:
-            return self.record_workflows[parent.workflow]
-        except AttributeError as e:
-            raise MissingWorkflowError("Workflow not found.", record=record) from e
+            return self.record_workflows[workflow_id]
         except KeyError as e:
             raise InvalidWorkflowError(
                 f"Workflow {parent.workflow} doesn't exist in the configuration.",
diff --git a/oarepo_workflows/requests/policy.py b/oarepo_workflows/requests/policy.py
index 4b539cf..c1f6f9f 100644
--- a/oarepo_workflows/requests/policy.py
+++ b/oarepo_workflows/requests/policy.py
@@ -17,6 +17,7 @@
 
 if TYPE_CHECKING:
     from flask_principal import Identity
+    from invenio_records_resources.records.api import Record
 
 
 class WorkflowRequestPolicy:
@@ -76,7 +77,7 @@ def items(self) -> list[tuple[str, WorkflowRequest]]:
         return ret
 
     def applicable_workflow_requests(
-        self, identity: Identity, **context: Any
+        self, identity: Identity, *, record: Record, **context: Any
     ) -> list[tuple[str, WorkflowRequest]]:
         """Return a list of applicable requests for the identity and context.
 
@@ -87,6 +88,6 @@ def applicable_workflow_requests(
         ret = []
 
         for name, request in self.items():
-            if request.is_applicable(identity, **context):
+            if request.is_applicable(identity, record=record, **context):
                 ret.append((name, request))
         return ret
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index d133dea..aadfc23 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -26,6 +26,7 @@
     from datetime import timedelta
 
     from invenio_records_permissions.generators import Generator
+    from invenio_records_resources.records.api import Record
     from invenio_requests.customizations.request_types import RequestType
 
     from oarepo_workflows.requests.events import WorkflowEvent
@@ -76,21 +77,21 @@ def recipient_entity_reference(self, **context: Any) -> dict | None:
         """
         return RecipientEntityReference(self, **context)
 
-    def is_applicable(self, identity: Identity, **context: Any) -> bool:
+    def is_applicable(self, identity: Identity, *, record: Record, **context: Any) -> bool:
         """Check if the request is applicable for the identity and context (which might include record, community, ...).
 
         :param identity: Identity of the requester.
         :param context: Context of the request that is passed to the requester generators.
         """
         try:
-            p = Permission(*self.requester_generator.needs(**context))
+            p = Permission(*self.requester_generator.needs(record=record, **context))
             if not p.needs:
                 return False
-            p.excludes.update(self.requester_generator.excludes(**context))
+            p.excludes.update(self.requester_generator.excludes(record=record, **context))
             if not p.allows(identity):
                 return False
-            if hasattr(self.request_type, "can_create"):
-                return self.request_type.can_create(identity, **context)
+            if hasattr(self.request_type, "is_applicable_to"):
+                return self.request_type.is_applicable_to(identity, topic=record, **context)
             return True
         except InvalidConfigurationError:
             raise
@@ -99,7 +100,7 @@ def is_applicable(self, identity: Identity, **context: Any) -> bool:
             return False
 
     @cached_property
-    def request_type(self) -> type[RequestType]:
+    def request_type(self) -> RequestType:
         """Return the request type for the workflow request."""
         if self._request_type is None:
             raise InvalidConfigurationError(

From e548b8369552abe0700b9db110f75683b4712b33 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Thu, 21 Nov 2024 10:17:06 +0100
Subject: [PATCH 17/21] re moved annotations.json, formatting

---
 annotations.json                      | 348 --------------------------
 oarepo_workflows/ext.py               |  20 +-
 oarepo_workflows/requests/requests.py |  12 +-
 3 files changed, 22 insertions(+), 358 deletions(-)
 delete mode 100644 annotations.json

diff --git a/annotations.json b/annotations.json
deleted file mode 100644
index 6f39b8e..0000000
--- a/annotations.json
+++ /dev/null
@@ -1,348 +0,0 @@
-[
-    {
-        "path": "oarepo_workflows/base.py",
-        "line": 23,
-        "func_name": "Workflow.requests",
-        "type_comments": [
-            "() -> tests.conftest.MyWorkflowRequests"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/records/systemfields/state.py",
-        "line": 15,
-        "func_name": "RecordStateField.post_create",
-        "type_comments": [
-            "(thesis.thesis.records.api.ThesisRecord) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/records/systemfields/state.py",
-        "line": 18,
-        "func_name": "RecordStateField.post_init",
-        "type_comments": [
-            "(thesis.thesis.records.api.ThesisRecord, Dict[str, str], thesis.records.models.ThesisMetadata) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/records/systemfields/state.py",
-        "line": 22,
-        "func_name": "RecordStateField.__get__",
-        "type_comments": [
-            "(thesis.thesis.records.api.ThesisRecord, invenio_records.systemfields.base.SystemFieldsMeta) -> None"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 44,
-        "func_name": "requester_generator",
-        "type_comments": [
-            "() -> oarepo_workflows.requests.policy.RequesterGenerator"
-        ],
-        "samples": 3
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 51,
-        "func_name": "WorkflowRequest.recipient_entity_reference",
-        "type_comments": [
-            "() -> Dict[str, str]"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 61,
-        "func_name": "WorkflowRequest.is_applicable",
-        "type_comments": [
-            "(flask_principal.Identity) -> bool"
-        ],
-        "samples": 6
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 151,
-        "func_name": "items",
-        "type_comments": [
-            "() -> List[Tuple[str, oarepo_workflows.requests.policy.WorkflowRequest]]"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 165,
-        "func_name": "WorkflowRequestPolicy.applicable_workflow_requests",
-        "type_comments": [
-            "(flask_principal.Identity) -> List[Tuple[str, oarepo_workflows.requests.policy.WorkflowRequest]]"
-        ],
-        "samples": 2
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 221,
-        "func_name": "RequesterGenerator.__init__",
-        "type_comments": [
-            "(List[oarepo_runtime.services.permissions.generators.UserWithRole]) -> pyannotate_runtime.collect_types.NoReturnType",
-            "(List[oarepo_runtime.services.permissions.generators.RecordOwners]) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 3
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 225,
-        "func_name": "RequesterGenerator.needs",
-        "type_comments": [
-            "() -> Set[flask_principal.Need]"
-        ],
-        "samples": 6
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 237,
-        "func_name": "RequesterGenerator.excludes",
-        "type_comments": [
-            "() -> Set"
-        ],
-        "samples": 6
-    },
-    {
-        "path": "oarepo_workflows/requests/policy.py",
-        "line": 264,
-        "func_name": "RecipientEntityReference",
-        "type_comments": [
-            "(oarepo_workflows.requests.policy.WorkflowRequest) -> Dict[str, str]"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "oarepo_workflows/services/permissions/generators.py",
-        "line": 32,
-        "func_name": "WorkflowPermission._get_workflow_id",
-        "type_comments": [
-            "(None) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 10
-    },
-    {
-        "path": "oarepo_workflows/services/permissions/generators.py",
-        "line": 43,
-        "func_name": "WorkflowPermission._get_permissions_from_workflow",
-        "type_comments": [
-            "(None, str) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 10
-    },
-    {
-        "path": "oarepo_workflows/services/permissions/generators.py",
-        "line": 52,
-        "func_name": "WorkflowPermission.needs",
-        "type_comments": [
-            "(None) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 10
-    },
-    {
-        "path": "tests/conftest.py",
-        "line": 153,
-        "func_name": "LoggedClient.__init__",
-        "type_comments": [
-            "(flask.testing.FlaskClient, pytest_invenio.user.UserFixtureBase) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 7
-    },
-    {
-        "path": "tests/conftest.py",
-        "line": 157,
-        "func_name": "LoggedClient._login",
-        "type_comments": [
-            "() -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 5
-    },
-    {
-        "path": "tests/conftest.py",
-        "line": 161,
-        "func_name": "LoggedClient.post",
-        "type_comments": [
-            "(*str) -> werkzeug.test.WrapperTestResponse",
-            "(*str) -> werkzeug.test.WrapperTestResponse",
-            "(*str) -> werkzeug.test.WrapperTestResponse",
-            "(*str) -> werkzeug.test.WrapperTestResponse"
-        ],
-        "samples": 5
-    },
-    {
-        "path": "tests/conftest.py",
-        "line": 180,
-        "func_name": "_logged_client",
-        "type_comments": [
-            "(pytest_invenio.user.UserFixtureBase) -> tests.conftest.LoggedClient"
-        ],
-        "samples": 7
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 8,
-        "func_name": "test_workflow_read",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 42,
-        "func_name": "test_workflow_publish",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 67,
-        "func_name": "test_query_filter",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 98,
-        "func_name": "test_invalid_workflow_input",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 115,
-        "func_name": "test_state_change",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], werkzeug.local.LocalProxy, method, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 123,
-        "func_name": "test_set_workflow",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, method, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 142,
-        "func_name": "test_state_change_entrypoint_hookup",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], werkzeug.local.LocalProxy, method, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow.py",
-        "line": 150,
-        "func_name": "test_set_workflow_entrypoint_hookup",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, method, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_field.py",
-        "line": 4,
-        "func_name": "test_workflow_read",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, Dict[str, Dict[str, str]], werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 15,
-        "func_name": "TestRecipient.reference_receivers",
-        "type_comments": [
-            "(thesis.thesis.records.api.ThesisRecord, None) -> List[Dict[str, str]]"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 21,
-        "func_name": "NullRecipient.reference_receivers",
-        "type_comments": [
-            "(thesis.thesis.records.api.ThesisRecord, None) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 25,
-        "func_name": "test_workflow_requests",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 34,
-        "func_name": "test_request_policy_access",
-        "type_comments": [
-            "(invenio_app.factory:flask.Flask) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 39,
-        "func_name": "test_is_applicable",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 54,
-        "func_name": "test_list_applicable_requests",
-        "type_comments": [
-            "(List[pytest_invenio.user.UserFixtureBase], function, werkzeug.local.LocalProxy, werkzeug.local.LocalProxy) -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "tests/test_workflow_requests.py",
-        "line": 55,
-        "func_name": "R",
-        "type_comments": [
-            "() -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 1
-    },
-    {
-        "path": "thesis/thesis/resources/records/config.py",
-        "line": 14,
-        "func_name": "response_handlers",
-        "type_comments": [
-            "() -> Dict[str, flask_resources.responses.ResponseHandler]"
-        ],
-        "samples": 5
-    },
-    {
-        "path": "thesis/thesis/resources/records/ui.py",
-        "line": 12,
-        "func_name": "ThesisUIJSONSerializer.__init__",
-        "type_comments": [
-            "() -> pyannotate_runtime.collect_types.NoReturnType"
-        ],
-        "samples": 5
-    }
-]
\ No newline at end of file
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 66f306f..813ebbb 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -218,23 +218,29 @@ def get_workflow(self, record: Record | dict) -> Workflow:
             try:
                 parent = record.parent  # noqa for typing: we do not have a better type for record with parent
             except AttributeError as e:
-                raise MissingWorkflowError("Record does not have a parent attribute, is it a draft-enabled record?",
-                                           record=record) from e
+                raise MissingWorkflowError(
+                    "Record does not have a parent attribute, is it a draft-enabled record?",
+                    record=record,
+                ) from e
             try:
                 workflow_id = parent.workflow
             except AttributeError as e:
-                raise MissingWorkflowError("Parent record does not have a workflow attribute.",
-                                           record=record) from e
+                raise MissingWorkflowError(
+                    "Parent record does not have a workflow attribute.", record=record
+                ) from e
         else:
             try:
                 parent = record["parent"]
             except KeyError as e:
-                raise MissingWorkflowError("Record does not have a parent attribute.",
-                                           record=record) from e
+                raise MissingWorkflowError(
+                    "Record does not have a parent attribute.", record=record
+                ) from e
             try:
                 workflow_id = parent["workflow"]
             except KeyError as e:
-                raise MissingWorkflowError("Parent record does not have a workflow attribute.", record=record) from e
+                raise MissingWorkflowError(
+                    "Parent record does not have a workflow attribute.", record=record
+                ) from e
 
         try:
             return self.record_workflows[workflow_id]
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index aadfc23..948fc54 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -77,7 +77,9 @@ def recipient_entity_reference(self, **context: Any) -> dict | None:
         """
         return RecipientEntityReference(self, **context)
 
-    def is_applicable(self, identity: Identity, *, record: Record, **context: Any) -> bool:
+    def is_applicable(
+        self, identity: Identity, *, record: Record, **context: Any
+    ) -> bool:
         """Check if the request is applicable for the identity and context (which might include record, community, ...).
 
         :param identity: Identity of the requester.
@@ -87,11 +89,15 @@ def is_applicable(self, identity: Identity, *, record: Record, **context: Any) -
             p = Permission(*self.requester_generator.needs(record=record, **context))
             if not p.needs:
                 return False
-            p.excludes.update(self.requester_generator.excludes(record=record, **context))
+            p.excludes.update(
+                self.requester_generator.excludes(record=record, **context)
+            )
             if not p.allows(identity):
                 return False
             if hasattr(self.request_type, "is_applicable_to"):
-                return self.request_type.is_applicable_to(identity, topic=record, **context)
+                return self.request_type.is_applicable_to(
+                    identity, topic=record, **context
+                )
             return True
         except InvalidConfigurationError:
             raise

From e655886a0837f231022f0ee3c845b7273232826d Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Thu, 21 Nov 2024 15:32:00 +0100
Subject: [PATCH 18/21] Version bump

---
 setup.cfg | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/setup.cfg b/setup.cfg
index 2de0b95..f09c803 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = oarepo-workflows
-version = 1.0.11
+version = 1.1.0
 description =
 authors = Ronald Krist <krist@cesnet.cz>
 readme = README.md

From 91a84da833d4125cb6e2baad3b3158a18c5bf877 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Thu, 21 Nov 2024 16:03:44 +0100
Subject: [PATCH 19/21] Fixed double permission check

---
 oarepo_workflows/requests/requests.py | 16 ++++++----------
 1 file changed, 6 insertions(+), 10 deletions(-)

diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index 948fc54..5e86a69 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -15,7 +15,7 @@
 from typing import TYPE_CHECKING, Any, Optional
 
 from flask_principal import Identity, Permission
-from invenio_requests.proxies import current_request_type_registry
+from invenio_requests.proxies import current_request_type_registry, current_requests_service
 
 from oarepo_workflows.errors import InvalidConfigurationError
 from oarepo_workflows.proxies import current_oarepo_workflows
@@ -86,19 +86,15 @@ def is_applicable(
         :param context: Context of the request that is passed to the requester generators.
         """
         try:
-            p = Permission(*self.requester_generator.needs(record=record, **context))
-            if not p.needs:
-                return False
-            p.excludes.update(
-                self.requester_generator.excludes(record=record, **context)
-            )
-            if not p.allows(identity):
-                return False
             if hasattr(self.request_type, "is_applicable_to"):
+                # the is_applicable_to must contain a permission check, so do not need to do any check here ...
                 return self.request_type.is_applicable_to(
                     identity, topic=record, **context
                 )
-            return True
+            else:
+                return current_requests_service.check_permission(
+                    identity, "create", record=record, request_type=self.request_type, **context
+                )
         except InvalidConfigurationError:
             raise
         except Exception as e:

From c86ed15fbeda93c520718a89e9c9a1a2a605e07e Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 22 Nov 2024 07:21:10 +0100
Subject: [PATCH 20/21] Moved the whole workflow request policy from
 oarepo-requests here

---
 oarepo_workflows/__init__.py                  |   4 +-
 oarepo_workflows/errors.py                    |  29 +++
 oarepo_workflows/ext.py                       |  54 +++++-
 oarepo_workflows/ext_config.py                |   2 +
 .../records/systemfields/workflow.py          |  14 ++
 oarepo_workflows/requests/__init__.py         |   7 +-
 oarepo_workflows/requests/events.py           |   2 +-
 .../requests/generators/__init__.py           |  25 +++
 .../{generators.py => generators/auto.py}     |  78 +-------
 .../requests/generators/conditionals.py       |  58 ++++++
 .../requests/generators/multiple.py           |  66 +++++++
 .../generators/recipient_generator.py         |  37 ++++
 .../requests/generators/workflow_based.py     | 173 ++++++++++++++++++
 oarepo_workflows/requests/permissions.py      |  43 +++++
 oarepo_workflows/requests/requests.py         |  17 +-
 .../services/permissions/generators.py        |   4 +-
 tests/conftest.py                             |   9 +
 tests/test_auto_approve.py                    |   2 +-
 tests/test_auto_request.py                    |   4 +-
 tests/test_workflow_requests.py               |  22 ++-
 20 files changed, 545 insertions(+), 105 deletions(-)
 create mode 100644 oarepo_workflows/requests/generators/__init__.py
 rename oarepo_workflows/requests/{generators.py => generators/auto.py} (50%)
 create mode 100644 oarepo_workflows/requests/generators/conditionals.py
 create mode 100644 oarepo_workflows/requests/generators/multiple.py
 create mode 100644 oarepo_workflows/requests/generators/recipient_generator.py
 create mode 100644 oarepo_workflows/requests/generators/workflow_based.py
 create mode 100644 oarepo_workflows/requests/permissions.py

diff --git a/oarepo_workflows/__init__.py b/oarepo_workflows/__init__.py
index 7f36821..7c5aaf6 100644
--- a/oarepo_workflows/__init__.py
+++ b/oarepo_workflows/__init__.py
@@ -32,10 +32,10 @@
     "WorkflowPermission",
     "WorkflowRecordPermissionPolicy",
     "WorkflowRequestPolicy",
-    "AutoApprove",
-    "AutoRequest",
     "WorkflowRequest",
     "WorkflowRequestEscalation",
     "WorkflowTransitions",
     "FromRecordWorkflow",
+    "AutoApprove",
+    "AutoRequest",
 )
diff --git a/oarepo_workflows/errors.py b/oarepo_workflows/errors.py
index 8de5452..bf54d07 100644
--- a/oarepo_workflows/errors.py
+++ b/oarepo_workflows/errors.py
@@ -76,3 +76,32 @@ def __init__(
 
 class InvalidConfigurationError(Exception):
     """Exception raised when a configuration is invalid."""
+
+
+class EventTypeNotInWorkflow(Exception):
+    """Exception raised when user tries to create a request with a request type that is not defined in the workflow."""
+
+    def __init__(self, request_type: str, event_type: str, workflow_code: str) -> None:
+        """Initialize the exception."""
+        self.request_type = request_type
+        self.workflow = workflow_code
+        self.event_type = event_type
+
+    @property
+    def description(self) -> str:
+        """Exception's description."""
+        return f"Event type {self.event_type} is not on request type {self.request_type} in workflow {self.workflow}."
+
+
+class RequestTypeNotInWorkflow(Exception):
+    """Exception raised when user tries to create a request with a request type that is not defined in the workflow."""
+
+    def __init__(self, request_type: str, workflow_code: str) -> None:
+        """Initialize the exception."""
+        self.request_type = request_type
+        self.workflow = workflow_code
+
+    @property
+    def description(self) -> str:
+        """Exception's description."""
+        return f"Request type {self.request_type} not in workflow {self.workflow}."
diff --git a/oarepo_workflows/ext.py b/oarepo_workflows/ext.py
index 813ebbb..06c9de9 100644
--- a/oarepo_workflows/ext.py
+++ b/oarepo_workflows/ext.py
@@ -14,7 +14,6 @@
 
 import importlib_metadata
 from invenio_drafts_resources.services.records.uow import ParentRecordCommitOp
-from invenio_records_resources.records import Record
 from invenio_records_resources.services.uow import RecordCommitOp, unit_of_work
 from oarepo_runtime.datastreams.utils import get_record_service_for_record
 
@@ -28,7 +27,7 @@
 if TYPE_CHECKING:
     from flask import Flask
     from flask_principal import Identity
-    from invenio_drafts_resources.records import ParentRecord
+    from invenio_drafts_resources.records import ParentRecord, Record
     from invenio_records_resources.services.uow import UnitOfWork
 
     from oarepo_workflows.base import (
@@ -36,6 +35,7 @@
         Workflow,
         WorkflowChangeNotifier,
     )
+    from oarepo_workflows.records.systemfields.workflow import WithWorkflow
 
 
 class OARepoWorkflows:
@@ -71,6 +71,11 @@ def init_config(self, app: Flask) -> None:
 
         app.config.setdefault("WORKFLOWS", ext_config.WORKFLOWS)
 
+        app.config.setdefault(
+            "OAREPO_WORKFLOWS_SET_REQUEST_PERMISSIONS",
+            ext_config.OAREPO_WORKFLOWS_SET_REQUEST_PERMISSIONS,
+        )
+
     def init_services(self) -> None:
         """Initialize workflow services."""
         # noinspection PyAttributeOutsideInit
@@ -214,29 +219,29 @@ def get_workflow(self, record: Record | dict) -> Workflow:
         :raises MissingWorkflowError: if the workflow is not found
         :raises InvalidWorkflowError: if the workflow is invalid
         """
-        if isinstance(record, Record):
+        if hasattr(record, "parent"):
             try:
-                parent = record.parent  # noqa for typing: we do not have a better type for record with parent
+                record_parent: WithWorkflow = record.parent  # noqa for typing: we do not have a better type for record with parent
             except AttributeError as e:
                 raise MissingWorkflowError(
                     "Record does not have a parent attribute, is it a draft-enabled record?",
                     record=record,
                 ) from e
             try:
-                workflow_id = parent.workflow
+                workflow_id = record_parent.workflow
             except AttributeError as e:
                 raise MissingWorkflowError(
                     "Parent record does not have a workflow attribute.", record=record
                 ) from e
         else:
             try:
-                parent = record["parent"]
+                dict_parent: dict = record["parent"]
             except KeyError as e:
                 raise MissingWorkflowError(
                     "Record does not have a parent attribute.", record=record
                 ) from e
             try:
-                workflow_id = parent["workflow"]
+                workflow_id = dict_parent["workflow"]
             except KeyError as e:
                 raise MissingWorkflowError(
                     "Parent record does not have a workflow attribute.", record=record
@@ -246,7 +251,7 @@ def get_workflow(self, record: Record | dict) -> Workflow:
             return self.record_workflows[workflow_id]
         except KeyError as e:
             raise InvalidWorkflowError(
-                f"Workflow {parent.workflow} doesn't exist in the configuration.",
+                f"Workflow {workflow_id} doesn't exist in the configuration.",
                 record=record,
             ) from e
 
@@ -273,3 +278,36 @@ def finalize_app(app: Flask) -> None:
         ext.auto_approve_service,
         service_id=ext.auto_approve_service.config.service_id,
     )
+
+    if app.config["OAREPO_WORKFLOWS_SET_REQUEST_PERMISSIONS"]:
+        patch_request_permissions(app)
+
+
+def patch_request_permissions(app: Flask) -> None:
+    """Replace invenio request permissions.
+
+    If permissions for requests are the plain invenio permissions,
+    replace those with workflow-based ones. If user set their own
+    permissions, keep those intact.
+    """
+    # patch invenio requests
+    from invenio_requests.services.permissions import (
+        PermissionPolicy as OriginalPermissionPolicy,
+    )
+
+    with app.app_context():
+        from invenio_requests.proxies import current_requests_service
+
+        from oarepo_workflows.requests.permissions import (
+            CreatorsFromWorkflowRequestsPermissionPolicy,
+        )
+
+        current_permission_policy = app.config.get("REQUESTS_PERMISSION_POLICY")
+        if current_permission_policy is OriginalPermissionPolicy:
+            app.config["REQUESTS_PERMISSION_POLICY"] = (
+                CreatorsFromWorkflowRequestsPermissionPolicy
+            )
+            assert (
+                current_requests_service.config.permission_policy_cls
+                is CreatorsFromWorkflowRequestsPermissionPolicy
+            )
diff --git a/oarepo_workflows/ext_config.py b/oarepo_workflows/ext_config.py
index 36c1c76..c8296ec 100644
--- a/oarepo_workflows/ext_config.py
+++ b/oarepo_workflows/ext_config.py
@@ -25,3 +25,5 @@
 
 WORKFLOWS: dict[str, Workflow] = {}
 """Configuration of workflows, must be provided by the user inside, for example, invenio.cfg."""
+
+OAREPO_WORKFLOWS_SET_REQUEST_PERMISSIONS = True
diff --git a/oarepo_workflows/records/systemfields/workflow.py b/oarepo_workflows/records/systemfields/workflow.py
index 88606f8..3018b10 100644
--- a/oarepo_workflows/records/systemfields/workflow.py
+++ b/oarepo_workflows/records/systemfields/workflow.py
@@ -9,10 +9,24 @@
 
 from __future__ import annotations
 
+from typing import Protocol
+
 from invenio_records.systemfields.model import ModelField
 from oarepo_runtime.records.systemfields import MappingSystemFieldMixin
 
 
+class WithWorkflow(Protocol):
+    """A protocol for a record's parent containing a workflow field.
+
+    Later on, if typing.Intersection is implemented,
+    one could use it to have the record correctly typed as
+    record: Intersection[WithWorkflow, ParentRecord]
+    """
+
+    workflow: str
+    """Workflow of the record."""
+
+
 class WorkflowField(MappingSystemFieldMixin, ModelField):
     """Workflow system field, should be defined on ParentRecord."""
 
diff --git a/oarepo_workflows/requests/__init__.py b/oarepo_workflows/requests/__init__.py
index 296bab3..925d14d 100644
--- a/oarepo_workflows/requests/__init__.py
+++ b/oarepo_workflows/requests/__init__.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from .generators import AutoApprove, AutoRequest, RecipientGeneratorMixin
+from .generators import AutoApprove, AutoRequest
 from .policy import (
     WorkflowRequestPolicy,
 )
@@ -22,11 +22,10 @@
 
 __all__ = (
     "WorkflowRequestPolicy",
-    "AutoApprove",
-    "AutoRequest",
-    "RecipientGeneratorMixin",
     "RecipientEntityReference",
     "WorkflowRequest",
     "WorkflowRequestEscalation",
     "WorkflowTransitions",
+    "AutoApprove",
+    "AutoRequest",
 )
diff --git a/oarepo_workflows/requests/events.py b/oarepo_workflows/requests/events.py
index 012bc5e..ff64c18 100644
--- a/oarepo_workflows/requests/events.py
+++ b/oarepo_workflows/requests/events.py
@@ -13,7 +13,7 @@
 from functools import cached_property
 from typing import TYPE_CHECKING
 
-from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
+from oarepo_workflows.requests.generators.multiple import MultipleGeneratorsGenerator
 
 if TYPE_CHECKING:
     from invenio_records_permissions.generators import Generator
diff --git a/oarepo_workflows/requests/generators/__init__.py b/oarepo_workflows/requests/generators/__init__.py
new file mode 100644
index 0000000..077732d
--- /dev/null
+++ b/oarepo_workflows/requests/generators/__init__.py
@@ -0,0 +1,25 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Need generators."""
+
+from .auto import AutoApprove, AutoRequest, auto_approve_need, auto_request_need
+from .conditionals import IfEventType, IfRequestType, IfRequestTypeBase
+from .multiple import MultipleGeneratorsGenerator
+from .recipient_generator import RecipientGeneratorMixin
+
+__all__ = (
+    "AutoApprove",
+    "auto_approve_need",
+    "AutoRequest",
+    "auto_request_need",
+    "IfEventType",
+    "IfRequestType",
+    "IfRequestTypeBase",
+    "MultipleGeneratorsGenerator",
+    "RecipientGeneratorMixin",
+)
diff --git a/oarepo_workflows/requests/generators.py b/oarepo_workflows/requests/generators/auto.py
similarity index 50%
rename from oarepo_workflows/requests/generators.py
rename to oarepo_workflows/requests/generators/auto.py
index b1b57d6..8b73e59 100644
--- a/oarepo_workflows/requests/generators.py
+++ b/oarepo_workflows/requests/generators/auto.py
@@ -5,70 +5,21 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
-"""Need generators."""
+"""Auto request and auto approve generators."""
 
 from __future__ import annotations
 
-import dataclasses
-from collections.abc import Iterable
 from typing import TYPE_CHECKING, Any, Optional
 
 from invenio_access import SystemRoleNeed
 from invenio_records_permissions.generators import Generator
 
+from .recipient_generator import RecipientGeneratorMixin
+
 if TYPE_CHECKING:
     from flask_principal import Need
     from invenio_records_resources.records import Record
-    from invenio_requests.customizations.request_types import RequestType
-
-
-@dataclasses.dataclass
-class MultipleGeneratorsGenerator(Generator):
-    """A generator that combines multiple generators with 'or' operation."""
-
-    generators: list[Generator] | tuple[Generator]
-    """List of generators to be combined."""
-
-    def needs(self, **context: Any) -> set[Need]:
-        """Generate a set of needs from generators that a person needs to have.
-
-        :param context: Context.
-        :return: Set of needs.
-        """
-        return {
-            need for generator in self.generators for need in generator.needs(**context)
-        }
-
-    def excludes(self, **context: Any) -> set[Need]:
-        """Generate a set of needs that person must not have.
-
-        :param context: Context.
-        :return: Set of needs.
-        """
-        return {
-            exclude
-            for generator in self.generators
-            for exclude in generator.excludes(**context)
-        }
-
-    def query_filter(self, **context: Any) -> list[dict]:
-        """Generate a list of opensearch query filters.
-
-         These filters are used to filter objects. These objects are governed by a policy
-         containing this generator.
-
-        :param context: Context.
-        """
-        ret: list[dict] = []
-        for generator in self.generators:
-            query_filter = generator.query_filter(**context)
-            if query_filter:
-                if isinstance(query_filter, Iterable):
-                    ret.extend(query_filter)
-                else:
-                    ret.append(query_filter)
-        return ret
-
+    from invenio_requests.customizations import RequestType
 
 auto_request_need = SystemRoleNeed("auto_request")
 auto_approve_need = SystemRoleNeed("auto_approve")
@@ -86,27 +37,6 @@ def needs(self, **context: Any) -> list[Need]:
         return [auto_request_need]
 
 
-class RecipientGeneratorMixin:
-    """Mixin for permission generators that can be used as recipients in WorkflowRequest."""
-
-    def reference_receivers(
-        self,
-        record: Optional[Record] = None,
-        request_type: Optional[RequestType] = None,
-        **context: Any,
-    ) -> list[dict[str, str]]:  # pragma: no cover
-        """Return the reference receiver(s) of the request.
-
-        This call requires the context to contain at least "record" and "request_type"
-
-        Must return a list of dictionary serialization of the receivers.
-
-        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 AutoApprove(RecipientGeneratorMixin, Generator):
     """Auto approve generator.
 
diff --git a/oarepo_workflows/requests/generators/conditionals.py b/oarepo_workflows/requests/generators/conditionals.py
new file mode 100644
index 0000000..ab22884
--- /dev/null
+++ b/oarepo_workflows/requests/generators/conditionals.py
@@ -0,0 +1,58 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Conditional generators based on request and event types."""
+
+from __future__ import annotations
+
+import abc
+from typing import TYPE_CHECKING, Any
+
+from invenio_records_permissions.generators import ConditionalGenerator, Generator
+
+if TYPE_CHECKING:
+    from invenio_requests.customizations import EventType, RequestType
+
+
+class IfRequestTypeBase(abc.ABC, ConditionalGenerator):
+    """Base class for conditional generators that generate needs based on request type."""
+
+    def __init__(
+        self, request_types: list[str] | tuple[str] | str, then_: list[Generator]
+    ) -> None:
+        """Initialize the generator."""
+        super().__init__(then_, else_=[])
+        if not isinstance(request_types, (list, tuple)):
+            request_types = [request_types]
+        self.request_types = request_types
+
+
+class IfRequestType(IfRequestTypeBase):
+    """Conditional generator that generates needs when a current request is of a given type."""
+
+    def _condition(self, request_type: RequestType, **kwargs: Any) -> bool:
+        return request_type.type_id in self.request_types
+
+
+class IfEventType(ConditionalGenerator):
+    """Conditional generator that generates needs when a current event is of a given type."""
+
+    def __init__(
+        self,
+        event_types: list[str] | tuple[str] | str,
+        then_: list[Generator],
+        else_: list[Generator] | None = None,
+    ) -> None:
+        """Initialize the generator."""
+        else_ = [] if else_ is None else else_
+        super().__init__(then_, else_=else_)
+        if not isinstance(event_types, (list, tuple)):
+            event_types = [event_types]
+        self.event_types = event_types
+
+    def _condition(self, event_type: EventType, **kwargs: Any) -> bool:
+        return event_type.type_id in self.event_types
diff --git a/oarepo_workflows/requests/generators/multiple.py b/oarepo_workflows/requests/generators/multiple.py
new file mode 100644
index 0000000..353846a
--- /dev/null
+++ b/oarepo_workflows/requests/generators/multiple.py
@@ -0,0 +1,66 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Generator that combines multiple generators together with an 'or' operation."""
+
+from __future__ import annotations
+
+import dataclasses
+from typing import TYPE_CHECKING, Any, Iterable
+
+from invenio_records_permissions.generators import Generator
+
+if TYPE_CHECKING:
+    from flask_principal import Need
+
+
+@dataclasses.dataclass
+class MultipleGeneratorsGenerator(Generator):
+    """A generator that combines multiple generators with 'or' operation."""
+
+    generators: list[Generator] | tuple[Generator]
+    """List of generators to be combined."""
+
+    def needs(self, **context: Any) -> set[Need]:
+        """Generate a set of needs from generators that a person needs to have.
+
+        :param context: Context.
+        :return: Set of needs.
+        """
+        return {
+            need for generator in self.generators for need in generator.needs(**context)
+        }
+
+    def excludes(self, **context: Any) -> set[Need]:
+        """Generate a set of needs that person must not have.
+
+        :param context: Context.
+        :return: Set of needs.
+        """
+        return {
+            exclude
+            for generator in self.generators
+            for exclude in generator.excludes(**context)
+        }
+
+    def query_filter(self, **context: Any) -> list[dict]:
+        """Generate a list of opensearch query filters.
+
+         These filters are used to filter objects. These objects are governed by a policy
+         containing this generator.
+
+        :param context: Context.
+        """
+        ret: list[dict] = []
+        for generator in self.generators:
+            query_filter = generator.query_filter(**context)
+            if query_filter:
+                if isinstance(query_filter, Iterable):
+                    ret.extend(query_filter)
+                else:
+                    ret.append(query_filter)
+        return ret
diff --git a/oarepo_workflows/requests/generators/recipient_generator.py b/oarepo_workflows/requests/generators/recipient_generator.py
new file mode 100644
index 0000000..f64b0f5
--- /dev/null
+++ b/oarepo_workflows/requests/generators/recipient_generator.py
@@ -0,0 +1,37 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Mixin for permission generators that can be used as recipients in WorkflowRequest."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, Optional
+
+if TYPE_CHECKING:
+    from invenio_records_resources.records import Record
+    from invenio_requests.customizations import RequestType
+
+
+class RecipientGeneratorMixin:
+    """Mixin for permission generators that can be used as recipients in WorkflowRequest."""
+
+    def reference_receivers(
+        self,
+        record: Optional[Record] = None,
+        request_type: Optional[RequestType] = None,
+        **context: Any,
+    ) -> list[dict[str, str]]:  # pragma: no cover
+        """Return the reference receiver(s) of the request.
+
+        This call requires the context to contain at least "record" and "request_type"
+
+        Must return a list of dictionary serialization of the receivers.
+
+        Might return empty list or None to indicate that the generator does not
+        provide any receivers.
+        """
+        raise NotImplementedError("Implement reference receiver in your code")
diff --git a/oarepo_workflows/requests/generators/workflow_based.py b/oarepo_workflows/requests/generators/workflow_based.py
new file mode 100644
index 0000000..788635f
--- /dev/null
+++ b/oarepo_workflows/requests/generators/workflow_based.py
@@ -0,0 +1,173 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Generators for needs/excludes/queries based on workflows."""
+
+from __future__ import annotations
+
+from typing import TYPE_CHECKING, Any, override
+
+from opensearch_dsl.query import Query
+
+from oarepo_workflows import FromRecordWorkflow, Workflow, WorkflowRequest
+from oarepo_workflows.errors import (
+    EventTypeNotInWorkflow,
+    InvalidWorkflowError,
+    MissingWorkflowError,
+    RequestTypeNotInWorkflow,
+)
+from oarepo_workflows.proxies import current_oarepo_workflows
+
+if TYPE_CHECKING:
+    from flask_principal import Need
+    from invenio_records_permissions.generators import Generator
+    from invenio_records_resources.records import Record
+    from invenio_requests.customizations import EventType, RequestType
+    from invenio_requests.records.api import Request
+
+
+class MissingTopicError(ValueError):
+    """Raised when the topic is missing in the request generator arguments."""
+
+    pass
+
+
+def _get_workflow_code_from_workflow(workflow: Workflow) -> str:
+    """Return the workflow code from the workflow."""
+    workflow_code = next(
+        iter(
+            k
+            for k, v in current_oarepo_workflows.record_workflows.items()
+            if v is workflow
+        )
+    )
+    return workflow_code
+
+
+class RequestPolicyWorkflowCreators(FromRecordWorkflow):
+    """Base class that generates creators from a workflow request."""
+
+    def _kwargs_parser(self, **kwargs: Any) -> dict[str, Any]:
+        """Transform the kwargs for subsequent methods."""
+        return kwargs
+
+    def _requester_generator(self, **kwargs: Any) -> Generator:
+        """Return the requesters as a single requester generator."""
+        raise NotImplementedError()
+
+    def _get_workflow_request(
+        self, request_type_id: str, **kwargs: Any
+    ) -> tuple[Workflow, WorkflowRequest]:
+        """Return the workflow request from the context."""
+        if "record" not in kwargs:
+            raise MissingTopicError(
+                "Topic not found in request permissions generator arguments, can't get workflow."
+            )
+        record = kwargs["record"]
+        workflow = current_oarepo_workflows.get_workflow(record)
+        workflow_requests = workflow.requests()
+        try:
+            workflow_request = workflow_requests[request_type_id]
+        except KeyError as e:
+            workflow_code = _get_workflow_code_from_workflow(workflow)
+            raise RequestTypeNotInWorkflow(
+                request_type=request_type_id,
+                workflow_code=workflow_code,
+            ) from e
+        return workflow, workflow_request
+
+    # return empty needs on MissingTopicError
+    # match None in query filter
+    # excludes empty needs
+    def needs(self, **context: Any) -> list[Need]:  # type: ignore
+        """Return the needs generated by the workflow permission."""
+        try:
+            context = self._kwargs_parser(**context)
+            generator = self._requester_generator(**context)
+            return generator.needs(**context)
+        except (MissingWorkflowError, InvalidWorkflowError, MissingTopicError):
+            return []
+
+    def excludes(self, **context: Any) -> list[Need]:
+        """Return the needs excluded by the workflow permission."""
+        try:
+            context = self._kwargs_parser(**context)
+            generator = self._requester_generator(**context)
+            return generator.excludes(**context)
+        except (MissingWorkflowError, InvalidWorkflowError, MissingTopicError):
+            return []
+
+    # not tested
+    def query_filter(
+        self, record: Record = None, request_type: RequestType = None, **context: Any
+    ) -> Query:
+        """Return the query filter generated by the workflow permission."""
+        try:
+            context = self._kwargs_parser(**context)
+            generator = self._requester_generator(
+                record=record, request_type=request_type, **context
+            )
+            return generator.query_filter(
+                record=record, request_type=request_type, **context
+            )
+        except (MissingWorkflowError, InvalidWorkflowError, MissingTopicError):
+            return Query("match_none")
+
+
+class RequestCreatorsFromWorkflow(RequestPolicyWorkflowCreators):
+    """Generates creators from a workflow request to be used in the request 'create' permission."""
+
+    def __init__(self) -> None:
+        """Initialize the generator."""
+        super().__init__(action="create")
+
+    @override
+    def _requester_generator(self, **kwargs: Any) -> Generator:
+        request_type: RequestType = kwargs["request_type"]
+        workflow, workflow_request = self._get_workflow_request(
+            request_type.type_id, **kwargs
+        )
+        return workflow_request.requester_generator
+
+
+class EventCreatorsFromWorkflow(RequestPolicyWorkflowCreators):
+    """Generates creators from a workflow request to be used in the event 'create' permission."""
+
+    def __init__(self) -> None:
+        """Initialize the generator."""
+        super().__init__(action="create")
+
+    @override
+    def _kwargs_parser(self, **kwargs: Any) -> dict[str, Any]:
+        request: Request = kwargs["request"]
+        kwargs.setdefault("request_type", request.type)
+        try:
+            kwargs["record"] = request.topic.resolve()
+        except Exception as e:  # noqa              TODO: better exception catching here
+            raise MissingTopicError(
+                "Topic not found in request event permissions generator arguments, can't get workflow."
+            ) from e
+        return kwargs
+
+    @override
+    def _requester_generator(self, **kwargs: Any) -> Generator:
+        request_type: RequestType = kwargs["request_type"]
+        event_type: EventType = kwargs["event_type"]
+        workflow, workflow_request = self._get_workflow_request(
+            request_type.type_id,
+            **kwargs,
+        )
+        try:
+            workflow_event = workflow_request.allowed_events[event_type.type_id]
+        except KeyError as e:
+            workflow_code = _get_workflow_code_from_workflow(workflow)
+            raise EventTypeNotInWorkflow(
+                request_type=request_type.type_id,
+                workflow_code=workflow_code,
+                event_type=event_type.type_id,
+            ) from e
+        return workflow_event.submitter_generator
diff --git a/oarepo_workflows/requests/permissions.py b/oarepo_workflows/requests/permissions.py
new file mode 100644
index 0000000..d6ef963
--- /dev/null
+++ b/oarepo_workflows/requests/permissions.py
@@ -0,0 +1,43 @@
+#
+# Copyright (C) 2024 CESNET z.s.p.o.
+#
+# oarepo-workflows is free software; you can redistribute it and/or
+# modify it under the terms of the MIT License; see LICENSE file for more
+# details.
+#
+"""Base permission policy that overwrites invenio-requests."""
+
+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.requests.generators.conditionals import IfEventType
+from oarepo_workflows.requests.generators.workflow_based import (
+    EventCreatorsFromWorkflow,
+    RequestCreatorsFromWorkflow,
+)
+
+
+class CreatorsFromWorkflowRequestsPermissionPolicy(InvenioRequestsPermissionPolicy):
+    """Permissions for requests based on workflows.
+
+    This permission adds a special generator RequestCreatorsFromWorkflow() to the default permissions.
+    This generator takes a topic, gets the workflow from the topic and returns the generator for
+    creators defined on the WorkflowRequest.
+    """
+
+    can_create = [
+        SystemProcess(),
+        RequestCreatorsFromWorkflow(),
+    ]
+
+    can_create_comment = [
+        SystemProcess(),
+        IfEventType(
+            [LogEventType.type_id, CommentEventType.type_id], [Creator(), Receiver()]
+        ),
+        EventCreatorsFromWorkflow(),
+    ]
diff --git a/oarepo_workflows/requests/requests.py b/oarepo_workflows/requests/requests.py
index 5e86a69..a2336fb 100644
--- a/oarepo_workflows/requests/requests.py
+++ b/oarepo_workflows/requests/requests.py
@@ -14,17 +14,20 @@
 from logging import getLogger
 from typing import TYPE_CHECKING, Any, Optional
 
-from flask_principal import Identity, Permission
-from invenio_requests.proxies import current_request_type_registry, current_requests_service
+from invenio_requests.proxies import (
+    current_request_type_registry,
+    current_requests_service,
+)
 
 from oarepo_workflows.errors import InvalidConfigurationError
 from oarepo_workflows.proxies import current_oarepo_workflows
-from oarepo_workflows.requests import RecipientGeneratorMixin
-from oarepo_workflows.requests.generators import MultipleGeneratorsGenerator
+from oarepo_workflows.requests.generators import RecipientGeneratorMixin
+from oarepo_workflows.requests.generators.multiple import MultipleGeneratorsGenerator
 
 if TYPE_CHECKING:
     from datetime import timedelta
 
+    from flask_principal import Identity
     from invenio_records_permissions.generators import Generator
     from invenio_records_resources.records.api import Record
     from invenio_requests.customizations.request_types import RequestType
@@ -93,7 +96,11 @@ def is_applicable(
                 )
             else:
                 return current_requests_service.check_permission(
-                    identity, "create", record=record, request_type=self.request_type, **context
+                    identity,
+                    "create",
+                    record=record,
+                    request_type=self.request_type,
+                    **context,
                 )
         except InvalidConfigurationError:
             raise
diff --git a/oarepo_workflows/services/permissions/generators.py b/oarepo_workflows/services/permissions/generators.py
index 48809dd..03d57e3 100644
--- a/oarepo_workflows/services/permissions/generators.py
+++ b/oarepo_workflows/services/permissions/generators.py
@@ -102,13 +102,13 @@ def _get_permissions_from_workflow(
         policy = current_oarepo_workflows.record_workflows[workflow_id].permissions
         return policy(action_name, record=record, **context)
 
-    def needs(self, record: Optional[Record] = None, **context: Any) -> set[Need]:
+    def needs(self, record: Optional[Record] = None, **context: Any) -> list[Need]:
         """Return needs that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(
             self._action, record, **context
         ).needs
 
-    def excludes(self, **context: Any) -> set[Need]:
+    def excludes(self, **context: Any) -> list[Need]:
         """Return excludes that are generated by the workflow permission."""
         return self._get_permissions_from_workflow(self._action, **context).excludes
 
diff --git a/tests/conftest.py b/tests/conftest.py
index 9c78d3e..12d5446 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -57,6 +57,10 @@ class MyWorkflowRequests(WorkflowRequestPolicy):
     )
 
 
+class IsApplicableTestRequestPolicy(WorkflowRequestPolicy):
+    req = WorkflowRequest(requesters=[RecordOwners()], recipients=[])
+
+
 WORKFLOWS = {
     "my_workflow": Workflow(
         label=_("Default workflow"),
@@ -68,6 +72,11 @@ class MyWorkflowRequests(WorkflowRequestPolicy):
         permission_policy_cls=RecordOwnersReadTestWorkflowPermissionPolicy,
         request_policy_cls=MyWorkflowRequests,
     ),
+    "is_applicable_workflow": Workflow(
+        label=_("For testing is_applicable"),
+        permission_policy_cls=DefaultWorkflowPermissions,
+        request_policy_cls=IsApplicableTestRequestPolicy,
+    ),
 }
 
 
diff --git a/tests/test_auto_approve.py b/tests/test_auto_approve.py
index c56b132..3a3f698 100644
--- a/tests/test_auto_approve.py
+++ b/tests/test_auto_approve.py
@@ -5,7 +5,7 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
-from oarepo_workflows.requests.generators import AutoApprove
+from oarepo_workflows.requests import AutoApprove
 import pytest
 from invenio_requests.resolvers.registry import ResolverRegistry
 
diff --git a/tests/test_auto_request.py b/tests/test_auto_request.py
index be7a57d..02036af 100644
--- a/tests/test_auto_request.py
+++ b/tests/test_auto_request.py
@@ -5,8 +5,8 @@
 # modify it under the terms of the MIT License; see LICENSE file for more
 # details.
 #
-from oarepo_workflows import AutoRequest
-from oarepo_workflows.requests.generators import auto_request_need
+from oarepo_workflows.requests import AutoRequest
+from oarepo_workflows.requests.generators.auto import auto_request_need
 
 
 def test_auto_request_needs(app):
diff --git a/tests/test_workflow_requests.py b/tests/test_workflow_requests.py
index 5dab92e..0fc056d 100644
--- a/tests/test_workflow_requests.py
+++ b/tests/test_workflow_requests.py
@@ -11,7 +11,8 @@
 from oarepo_runtime.services.permissions import RecordOwners, UserWithRole
 
 from oarepo_workflows import WorkflowRequestPolicy, WorkflowTransitions
-from oarepo_workflows.requests import RecipientGeneratorMixin, WorkflowRequest
+from oarepo_workflows.requests import WorkflowRequest
+from oarepo_workflows.requests.generators import RecipientGeneratorMixin
 from thesis.records.api import ThesisRecord
 
 from flask_principal import Identity, UserNeed, RoleNeed
@@ -114,7 +115,11 @@ def test_is_applicable(
     id2 = Identity(id=2)
     id2.provides.add(UserNeed(2))
 
-    record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
+    record = SimpleNamespace(
+        parent=SimpleNamespace(
+            owners=[SimpleNamespace(id=1)], workflow="is_applicable_workflow"
+        )
+    )
     assert req.is_applicable(id2, record=record) is False
     assert req.is_applicable(id1, record=record) is True
 
@@ -131,15 +136,20 @@ def test_list_applicable_requests(
     id2.provides.add(UserNeed(2))
     id2.provides.add(RoleNeed("administrator"))
 
-    record = SimpleNamespace(parent=SimpleNamespace(owners=[SimpleNamespace(id=1)]))
+    record = SimpleNamespace(
+        parent=SimpleNamespace(
+            owners=[SimpleNamespace(id=1)], workflow="is_applicable_workflow"
+        )
+    )
 
     assert set(
         x[0] for x in requests.applicable_workflow_requests(id1, record=record)
     ) == {"req"}
 
-    assert set(
-        x[0] for x in requests.applicable_workflow_requests(id2, record=record)
-    ) == {"req1"}
+    assert (
+        set(x[0] for x in requests.applicable_workflow_requests(id2, record=record))
+        == set()
+    )
 
 
 def test_get_workflow_request_via_index(

From 2c96313c9982be99f64fe0cb25c4cdea5f80d796 Mon Sep 17 00:00:00 2001
From: Mirek Simek <miroslav.simek@gmail.com>
Date: Fri, 22 Nov 2024 07:39:39 +0100
Subject: [PATCH 21/21] Alias

---
 oarepo_workflows/requests/__init__.py | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/oarepo_workflows/requests/__init__.py b/oarepo_workflows/requests/__init__.py
index 925d14d..dab55e1 100644
--- a/oarepo_workflows/requests/__init__.py
+++ b/oarepo_workflows/requests/__init__.py
@@ -9,7 +9,7 @@
 
 from __future__ import annotations
 
-from .generators import AutoApprove, AutoRequest
+from .generators import AutoApprove, AutoRequest, RecipientGeneratorMixin
 from .policy import (
     WorkflowRequestPolicy,
 )
@@ -28,4 +28,5 @@
     "WorkflowTransitions",
     "AutoApprove",
     "AutoRequest",
+    "RecipientGeneratorMixin",
 )