From 82aebb9c2307f0db3442606e3adb1b74a849c33d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 12:28:34 +0100 Subject: [PATCH 1/7] Feat: Add metadata info to workflows --- CHANGELOG.rst | 3 ++- guillotina/contrib/workflows/api.py | 6 +++++- .../contrib/workflows/base/guillotina_basic.yaml | 12 ++++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e99f79544..b65c04e1f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,8 @@ CHANGELOG 7.0.1 (unreleased) ------------------ -- Nothing changed yet. +- Feat: Add metadata info to workflows + [rboixaderg] 7.0.0 (2023-12-06) diff --git a/guillotina/contrib/workflows/api.py b/guillotina/contrib/workflows/api.py index 7ce47b960..274e4fdd8 100644 --- a/guillotina/contrib/workflows/api.py +++ b/guillotina/contrib/workflows/api.py @@ -32,7 +32,11 @@ async def __call__(self): return workflow async for action_name, action in workflow_obj.available_actions(self.request): workflow["transitions"].append( - {"@id": obj_url + "/@workflow/" + action_name, "title": action["title"]} + { + "@id": obj_url + "/@workflow/" + action_name, + "title": action["title"], + "translated_title": action.get("translated_title", {}), + } ) workflow_obj = query_adapter(self.context, IWorkflowBehavior) diff --git a/guillotina/contrib/workflows/base/guillotina_basic.yaml b/guillotina/contrib/workflows/base/guillotina_basic.yaml index 176782bc9..044c4f772 100644 --- a/guillotina/contrib/workflows/base/guillotina_basic.yaml +++ b/guillotina/contrib/workflows/base/guillotina_basic.yaml @@ -1,6 +1,12 @@ initial_state: private states: private: + metadata: + title: Private + translated_title: + en: Private + ca: Privat + es: Privado actions: publish: title: Publish @@ -18,6 +24,12 @@ states: role: guillotina.Anonymous permission: guillotina.SearchContent public: + metadata: + title: Public + translated_title: + en: Public + ca: Públic + es: Público actions: retire: title: Retire From 0eb7e3c08f3e815d51585c1a5dd4cb4f23ad8503 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 12:28:48 +0100 Subject: [PATCH 2/7] Fix: Update workflow vocabulary name --- CHANGELOG.rst | 4 ++- guillotina/contrib/workflows/interfaces.py | 4 +-- guillotina/contrib/workflows/vocabularies.py | 5 +++- .../tests/workflows/test_workflow_basic.py | 25 ++++++++++++++++--- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b65c04e1f..a5ae24209 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,7 +4,9 @@ CHANGELOG 7.0.1 (unreleased) ------------------ -- Feat: Add metadata info to workflows +- Feat: Add metadata info to workflows +- Fix: Update workflow vocabulary name +- Feat: Update workflow vocabulary title attribute to use metadata [rboixaderg] diff --git a/guillotina/contrib/workflows/interfaces.py b/guillotina/contrib/workflows/interfaces.py index c0d4648d4..a475571a3 100644 --- a/guillotina/contrib/workflows/interfaces.py +++ b/guillotina/contrib/workflows/interfaces.py @@ -33,7 +33,6 @@ class IWorkflowUtility(IAsyncUtility): class IWorkflow(Interface): - initial_state = Attribute("Initial state of the workflow") @@ -54,14 +53,13 @@ def __call__(self, context: IResource) -> Optional[str]: class IWorkflowBehavior(Interface): - index_field("review_state", store=True, type="keyword") review_state = schema.Choice( readonly=True, title="Workflow review state", required=False, defaultFactory=DefaultReviewState(), - source="worklow_states", + source="workflow_states", ) history = schema.List( diff --git a/guillotina/contrib/workflows/vocabularies.py b/guillotina/contrib/workflows/vocabularies.py index 458bbb714..a8bb97e74 100644 --- a/guillotina/contrib/workflows/vocabularies.py +++ b/guillotina/contrib/workflows/vocabularies.py @@ -6,7 +6,7 @@ from guillotina.interfaces import IResource -@configure.vocabulary(name="worklow_states") +@configure.vocabulary(name="workflow_states") class WorkflowVocabulary: def __init__(self, context): self.context = context @@ -35,6 +35,9 @@ def __len__(self): def getTerm(self, value): if value in self.states: + metadata = self.states[value].get("metadata", None) + if metadata: + return metadata return value else: raise KeyError("No valid state") diff --git a/guillotina/tests/workflows/test_workflow_basic.py b/guillotina/tests/workflows/test_workflow_basic.py index 520389a8c..dc9e03bca 100644 --- a/guillotina/tests/workflows/test_workflow_basic.py +++ b/guillotina/tests/workflows/test_workflow_basic.py @@ -14,6 +14,7 @@ async def test_workflow_basic(container_requester): async with container_requester as requester: response, _ = await requester("GET", "/db/guillotina/@workflow") assert response["transitions"][0]["title"] == "Publish" + assert response["transitions"][0]["translated_title"] == {} response, _ = await requester("GET", "/db/guillotina") assert response["review_state"] == "private" @@ -32,9 +33,27 @@ async def test_workflow_basic(container_requester): assert response["review_state"] == "public" response, _ = await requester("GET", "/db/guillotina/@sharing") - assert ( - response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" - ) + assert response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" response, status = await requester("GET", "/db/guillotina", token=None) assert status == 200 + + response, status = await requester("GET", "/db/guillotina/@vocabularies/workflow_states") + assert status == 200 + assert response == { + "@id": "http://localhost/db/guillotina/@vocabularies/workflow_states", + "items": [ + { + "title": { + "title": "Private", + "translated_title": {"en": "Private", "ca": "Privat", "es": "Privado"}, + }, + "token": "private", + }, + { + "title": {"title": "Public", "translated_title": {"en": "Public", "ca": "Públic", "es": "Público"}}, + "token": "public", + }, + ], + "items_total": 2, + } From 7d48a0303507e793daf070bdd3969ff9eb129591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 12:31:45 +0100 Subject: [PATCH 3/7] chore: black --- guillotina/tests/workflows/test_workflow_basic.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/guillotina/tests/workflows/test_workflow_basic.py b/guillotina/tests/workflows/test_workflow_basic.py index dc9e03bca..1cafeacc6 100644 --- a/guillotina/tests/workflows/test_workflow_basic.py +++ b/guillotina/tests/workflows/test_workflow_basic.py @@ -33,7 +33,9 @@ async def test_workflow_basic(container_requester): assert response["review_state"] == "public" response, _ = await requester("GET", "/db/guillotina/@sharing") - assert response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" + assert ( + response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" + ) response, status = await requester("GET", "/db/guillotina", token=None) assert status == 200 @@ -51,7 +53,10 @@ async def test_workflow_basic(container_requester): "token": "private", }, { - "title": {"title": "Public", "translated_title": {"en": "Public", "ca": "Públic", "es": "Público"}}, + "title": { + "title": "Public", + "translated_title": {"en": "Public", "ca": "Públic", "es": "Público"}, + }, "token": "public", }, ], From cace7e8fbe29b3d9d9ecaea79172eaee1db195b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 16:22:13 +0100 Subject: [PATCH 4/7] feat: use metadata in actions instead of translated title --- guillotina/contrib/workflows/api.py | 2 +- guillotina/tests/workflows/test_workflow_basic.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/guillotina/contrib/workflows/api.py b/guillotina/contrib/workflows/api.py index 274e4fdd8..c12e31da5 100644 --- a/guillotina/contrib/workflows/api.py +++ b/guillotina/contrib/workflows/api.py @@ -35,7 +35,7 @@ async def __call__(self): { "@id": obj_url + "/@workflow/" + action_name, "title": action["title"], - "translated_title": action.get("translated_title", {}), + "metadata": action.get("metadata", {}), } ) diff --git a/guillotina/tests/workflows/test_workflow_basic.py b/guillotina/tests/workflows/test_workflow_basic.py index 1cafeacc6..8252e1598 100644 --- a/guillotina/tests/workflows/test_workflow_basic.py +++ b/guillotina/tests/workflows/test_workflow_basic.py @@ -14,7 +14,7 @@ async def test_workflow_basic(container_requester): async with container_requester as requester: response, _ = await requester("GET", "/db/guillotina/@workflow") assert response["transitions"][0]["title"] == "Publish" - assert response["transitions"][0]["translated_title"] == {} + assert response["transitions"][0]["metadata"] == {} response, _ = await requester("GET", "/db/guillotina") assert response["review_state"] == "private" @@ -33,9 +33,7 @@ async def test_workflow_basic(container_requester): assert response["review_state"] == "public" response, _ = await requester("GET", "/db/guillotina/@sharing") - assert ( - response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" - ) + assert response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" response, status = await requester("GET", "/db/guillotina", token=None) assert status == 200 From 7d6710daf0bb266c448f5c7d31e18355d3e88eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 16:27:49 +0100 Subject: [PATCH 5/7] chore: black --- guillotina/tests/workflows/test_workflow_basic.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/guillotina/tests/workflows/test_workflow_basic.py b/guillotina/tests/workflows/test_workflow_basic.py index 8252e1598..cc8b07271 100644 --- a/guillotina/tests/workflows/test_workflow_basic.py +++ b/guillotina/tests/workflows/test_workflow_basic.py @@ -33,7 +33,9 @@ async def test_workflow_basic(container_requester): assert response["review_state"] == "public" response, _ = await requester("GET", "/db/guillotina/@sharing") - assert response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" + assert ( + response["local"]["roleperm"]["guillotina.Anonymous"]["guillotina.AccessContent"] == "AllowSingle" + ) response, status = await requester("GET", "/db/guillotina", token=None) assert status == 200 From c2ed516be31a5354f0a25b9aa7ba9860ed247f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Roger=20Boixader=20G=C3=BCell?= Date: Fri, 19 Jan 2024 17:23:14 +0100 Subject: [PATCH 6/7] test: use custom workflow for tests --- .../workflows/base/guillotina_basic.yaml | 8 -- .../tests/workflows/test_workflow_basic.py | 96 ++++++++++++++++++- 2 files changed, 94 insertions(+), 10 deletions(-) diff --git a/guillotina/contrib/workflows/base/guillotina_basic.yaml b/guillotina/contrib/workflows/base/guillotina_basic.yaml index 044c4f772..fa0ce3a7b 100644 --- a/guillotina/contrib/workflows/base/guillotina_basic.yaml +++ b/guillotina/contrib/workflows/base/guillotina_basic.yaml @@ -3,10 +3,6 @@ states: private: metadata: title: Private - translated_title: - en: Private - ca: Privat - es: Privado actions: publish: title: Publish @@ -26,10 +22,6 @@ states: public: metadata: title: Public - translated_title: - en: Public - ca: Públic - es: Público actions: retire: title: Retire diff --git a/guillotina/tests/workflows/test_workflow_basic.py b/guillotina/tests/workflows/test_workflow_basic.py index cc8b07271..efbb6da0b 100644 --- a/guillotina/tests/workflows/test_workflow_basic.py +++ b/guillotina/tests/workflows/test_workflow_basic.py @@ -3,18 +3,110 @@ pytestmark = pytest.mark.asyncio +guillotina_basic_with_translations = { + "initial_state": "private", + "states": { + "private": { + "metadata": { + "title": "Private", + "translated_title": { + "en": "Private", + "ca": "Privat", + "es": "Privado", + }, + }, + "actions": { + "publish": { + "title": "Publish", + "metadata": { + "translated_title": { + "en": "Publish", + "ca": "Publicar", + "es": "Publicar", + }, + }, + "to": "public", + "check_permission": "guillotina.ReviewContent", + } + }, + "set_permission": { + "roleperm": [ + { + "setting": "Deny", + "role": "guillotina.Anonymous", + "permission": "guillotina.ViewContent", + }, + { + "setting": "Deny", + "role": "guillotina.Anonymous", + "permission": "guillotina.AccessContent", + }, + { + "setting": "Deny", + "role": "guillotina.Anonymous", + "permission": "guillotina.SearchContent", + }, + ] + }, + }, + "public": { + "metadata": { + "title": "Public", + "translated_title": { + "en": "Public", + "ca": "Públic", + "es": "Público", + }, + }, + "actions": { + "retire": { + "title": "Retire", + "to": "private", + "check_permission": "guillotina.ReviewContent", + }, + }, + "set_permission": { + "roleperm": [ + { + "setting": "AllowSingle", + "role": "guillotina.Anonymous", + "permission": "guillotina.ViewContent", + }, + { + "setting": "AllowSingle", + "role": "guillotina.Anonymous", + "permission": "guillotina.AccessContent", + }, + { + "setting": "AllowSingle", + "role": "guillotina.Anonymous", + "permission": "guillotina.SearchContent", + }, + ] + }, + }, + }, +} + @pytest.mark.app_settings( { "applications": ["guillotina", "guillotina.contrib.workflows"], - "workflows_content": {"guillotina.interfaces.IContainer": "guillotina_basic"}, + "workflows_content": {"guillotina.interfaces.IContainer": "guillotina_basic_with_translations"}, + "workflows": {"guillotina_basic_with_translations": guillotina_basic_with_translations}, } ) async def test_workflow_basic(container_requester): async with container_requester as requester: response, _ = await requester("GET", "/db/guillotina/@workflow") assert response["transitions"][0]["title"] == "Publish" - assert response["transitions"][0]["metadata"] == {} + assert response["transitions"][0]["metadata"] == { + "translated_title": { + "en": "Publish", + "ca": "Publicar", + "es": "Publicar", + } + } response, _ = await requester("GET", "/db/guillotina") assert response["review_state"] == "private" From c270e12c0bb05be08f5c119182b433d2f22b4785 Mon Sep 17 00:00:00 2001 From: nilbacardit26 Date: Fri, 5 Jan 2024 18:41:00 +0100 Subject: [PATCH 7/7] wip --- guillotina/schema/_field.py | 8 ++++++++ guillotina/tests/test_api.py | 25 +++++++++++++++++++++++++ guillotina/tests/test_serialize.py | 6 ++++++ 3 files changed, 39 insertions(+) diff --git a/guillotina/schema/_field.py b/guillotina/schema/_field.py index 0f10fb5cc..20078c6df 100644 --- a/guillotina/schema/_field.py +++ b/guillotina/schema/_field.py @@ -241,6 +241,14 @@ class Time(Orderable, Field): __doc__ = ITime.__doc__ _type = time + def _validate(self, value): + try: + args = [int(unit_time) for unit_time in value.split(":")] + value = time(*args) + except Exception: + raise WrongType(value, self._type, self.__name__) + super(Time, self)._validate(value) + @implementer(IChoice, IFromUnicode) class Choice(Field): diff --git a/guillotina/tests/test_api.py b/guillotina/tests/test_api.py index cd6b9493a..3bf67b6f3 100644 --- a/guillotina/tests/test_api.py +++ b/guillotina/tests/test_api.py @@ -1,4 +1,5 @@ from datetime import datetime +from datetime import time from guillotina import configure from guillotina import schema from guillotina.addons import Addon @@ -1244,6 +1245,7 @@ class ITestSchema(Interface): object_a = schema.Object(IObjectA, required=False) list_object_a = PatchField(schema.List(value_type=schema.Object(IObjectA), required=False)) + time_ = schema.Time(min="9:00:00", max="11:00:00", required=False) @contenttype(type_name="TestSchema", schema=ITestSchema) @@ -1318,6 +1320,29 @@ async def test_field_values_list_bucket(container_requester): ) assert status == 410 +async def test_time_field_validation(container_requester): + async with container_requester as requester: + __import__("pdb").set_trace() + resp, status = await requester( + "POST", "/db/guillotina/", data=json.dumps({"@type": "TestSchema", "time_": "16:40:09", "id": "foo_item"}) + ) + assert status == 201 + + resp, status = await requester( + "GET", "/db/guillotina/foo_item", data=json.dumps({"@type": "TestSchema", "time_": "16:40:09"}) + ) + assert status == 200 + assert resp["time"] == "16:40:09" + + resp, status = await requester( + "POST", "/db/guillotina/", data=json.dumps({"@type": "TestSchema", "time_": "16:40:099", "id": "foo_item"}) + ) + assert status == 412 + + resp, status = await requester( + "POST", "/db/guillotina/", data=json.dumps({"@type": "TestSchema", "time_": 3600, "id": "foo_item"}) + ) + assert status == 412 async def test_patch_field_validation(container_requester): async with container_requester as requester: diff --git a/guillotina/tests/test_serialize.py b/guillotina/tests/test_serialize.py index 3e6f9e61b..0617b0af7 100644 --- a/guillotina/tests/test_serialize.py +++ b/guillotina/tests/test_serialize.py @@ -1,4 +1,5 @@ from datetime import datetime +from datetime import time from dateutil.tz import tzutc from guillotina import fields from guillotina import schema @@ -263,6 +264,11 @@ async def test_deserialize_datetime(dummy_guillotina): converted = schema_compatible(now.isoformat(), ITestSchema["datetime"]) assert converted.minute == now.minute +async def test_deserialize_time(dummy_guillotina): + now = time.fromisoformat("10:00:00") + converted = schema_compatible(now, ITestSchema["time"]) + assert converted.minute == now.minute + async def test_check_permission_deserialize_content(dummy_request): login()