From 5615c495954c4b9c284e45bf246c7507232b007f Mon Sep 17 00:00:00 2001 From: mutantsan Date: Fri, 26 Jan 2024 18:27:49 +0200 Subject: [PATCH 1/5] add the ability to create an arbitrary scheme for rendering elsewhere --- .../scheming/arbitrary_schema_example.yaml | 27 +++++ ckanext/scheming/helpers.py | 25 ++++ ckanext/scheming/plugins.py | 11 ++ .../templates/scheming/group/group_form.html | 8 +- .../scheming/organization/group_form.html | 8 +- .../package/snippets/package_form.html | 26 +--- .../package/snippets/resource_form.html | 29 +---- .../scheming/snippets/render_fields.html | 30 +++++ .../scheming/tests/test_arbitrary_schema.py | 30 +++++ ckanext/scheming/tests/test_helpers.py | 112 ++++++++++-------- setup.py | 3 +- test.ini | 3 +- 12 files changed, 194 insertions(+), 118 deletions(-) create mode 100644 ckanext/scheming/arbitrary_schema_example.yaml create mode 100644 ckanext/scheming/templates/scheming/snippets/render_fields.html create mode 100644 ckanext/scheming/tests/test_arbitrary_schema.py diff --git a/ckanext/scheming/arbitrary_schema_example.yaml b/ckanext/scheming/arbitrary_schema_example.yaml new file mode 100644 index 000000000..c66daba4d --- /dev/null +++ b/ckanext/scheming/arbitrary_schema_example.yaml @@ -0,0 +1,27 @@ +scheming_version: 2 +schema_id: ckanext_notifier +about: An example of a config schema for a fictional extension + +fields: + - field_name: ckanext.ckanext_notifier.enable_notifications + label: Enable notifications + validators: default(true) boolean_validator + preset: select + required: true + choices: + - value: true + label: Enable + - value: false + label: Disable + + - field_name: ckanext.ckanext_notifier.notify_to_email + label: Notification email + validators: unicode_safe email_validator + required: true + help_text: Specify the email address to which the notification will be sent + + - field_name: ckanext.ckanext_notifier.frequency + label: Notification frequency in seconds + validators: default(3600) int_validator + required: true + input_type: number diff --git a/ckanext/scheming/helpers.py b/ckanext/scheming/helpers.py index 25ba39690..9f2b71c8c 100644 --- a/ckanext/scheming/helpers.py +++ b/ckanext/scheming/helpers.py @@ -445,3 +445,28 @@ def scheming_flatten_subfield(subfield, data): for k in record: flat[prefix + k] = record[k] return flat + + +@helper +def scheming_arbitrary_schemas(expanded=True): + """ + Return the dict of arbitrary schemas. Or if scheming_arbitrary + plugin is not loaded return None. + """ + from ckanext.scheming.plugins import SchemingArbitraryPlugin as plugin + + if plugin.instance: + if expanded: + return plugin.instance._expanded_schemas + return plugin.instance._schemas + + return {} + + +@helper +def scheming_get_arbitrary_schema(schema_id, expanded=True): + """ + Return the schema for the schema_id passed or None if + no schema is defined for that schema_id. + """ + return scheming_arbitrary_schemas(expanded).get(schema_id) diff --git a/ckanext/scheming/plugins.py b/ckanext/scheming/plugins.py index 0980a6843..5ca300c3d 100644 --- a/ckanext/scheming/plugins.py +++ b/ckanext/scheming/plugins.py @@ -473,6 +473,17 @@ def get_actions(self): logic.scheming_organization_schema_show, } +class SchemingArbitraryPlugin(p.SingletonPlugin, _SchemingMixin): + p.implements(p.IConfigurer) + + SCHEMA_OPTION = "scheming.arbitrary_schemas" + FALLBACK_OPTION = 'scheming.arbitrary_fallback' + SCHEMA_TYPE_FIELD = "schema_id" + + @classmethod + def _store_instance(cls, self): + SchemingArbitraryPlugin.instance = self + class SchemingNerfIndexPlugin(p.SingletonPlugin): """ diff --git a/ckanext/scheming/templates/scheming/group/group_form.html b/ckanext/scheming/templates/scheming/group/group_form.html index b3cf2c9b4..753a16761 100644 --- a/ckanext/scheming/templates/scheming/group/group_form.html +++ b/ckanext/scheming/templates/scheming/group/group_form.html @@ -12,13 +12,7 @@
{{ h.csrf_input() if 'csrf_input' in h }} {%- set schema = h.scheming_get_group_schema(group_type) -%} - {%- for field in schema['fields'] -%} - {%- if field.form_snippet is not none -%} - {%- snippet 'scheming/snippets/form_field.html', - field=field, data=data, errors=errors, licenses=licenses, - entity_type='group', object_type=group_type -%} - {%- endif -%} - {%- endfor -%} + {% snippet 'scheming/snippets/render_fields.html', fields=schema.fields, data=data, errors=errors, entity_type='group', object_type=group_type %}
{% block delete_button %} diff --git a/ckanext/scheming/templates/scheming/organization/group_form.html b/ckanext/scheming/templates/scheming/organization/group_form.html index ebf78de76..d16be4a11 100644 --- a/ckanext/scheming/templates/scheming/organization/group_form.html +++ b/ckanext/scheming/templates/scheming/organization/group_form.html @@ -14,13 +14,7 @@ {{ h.csrf_input() if 'csrf_input' in h }} {%- set schema = h.scheming_get_organization_schema(group_type) -%} - {%- for field in schema['fields'] -%} - {%- if field.form_snippet is not none -%} - {%- snippet 'scheming/snippets/form_field.html', - field=field, data=data, errors=errors, licenses=licenses, - entity_type='organization', object_type=group_type -%} - {%- endif -%} - {%- endfor -%} + {% snippet 'scheming/snippets/render_fields.html', fields=schema.fields, data=data, errors=errors, entity_type='organization', object_type=group_type %} {{ form.required_message() }} diff --git a/ckanext/scheming/templates/scheming/package/snippets/package_form.html b/ckanext/scheming/templates/scheming/package/snippets/package_form.html index 6d96ce038..5b01b67cb 100644 --- a/ckanext/scheming/templates/scheming/package/snippets/package_form.html +++ b/ckanext/scheming/templates/scheming/package/snippets/package_form.html @@ -68,30 +68,8 @@ {%- else -%} {%- set fields = schema.dataset_fields -%} {%- endif -%} - {%- for field in fields -%} - {%- if field.form_snippet is not none -%} - {%- if field.field_name not in data %} - {# Set the field default value before rendering but only if - it doesn't already exist in data which would mean the form - has been submitted. #} - {% if field.default_jinja2 %} - {% do data.__setitem__( - field.field_name, - h.scheming_render_from_string(field.default_jinja2)) %} - {% elif field.default %} - {% do data.__setitem__(field.field_name, field.default) %} - {% endif %} - {% endif -%} - {%- snippet 'scheming/snippets/form_field.html', - field=field, - data=data, - errors=errors, - licenses=c.licenses, - entity_type='dataset', - object_type=dataset_type - -%} - {%- endif -%} - {%- endfor -%} + + {% snippet 'scheming/snippets/render_fields.html', fields=schema.dataset_fields, data=data, errors=errors, entity_type='dataset', object_type=dataset_type, set_fields_defaults=true %} {%- if pages -%} diff --git a/ckanext/scheming/templates/scheming/package/snippets/resource_form.html b/ckanext/scheming/templates/scheming/package/snippets/resource_form.html index 88d231b07..f08cbb411 100644 --- a/ckanext/scheming/templates/scheming/package/snippets/resource_form.html +++ b/ckanext/scheming/templates/scheming/package/snippets/resource_form.html @@ -47,34 +47,7 @@ {%- endif -%} {%- set schema = h.scheming_get_dataset_schema(dataset_type) -%} - {%- for field in schema.resource_fields -%} - {%- if field.form_snippet is not none -%} - {%- if field.field_name not in data %} - {# Set the field default value before rendering but only if - it doesn't already exist in data which would mean the form - has been submitted. #} - {% if field.default_jinja2 %} - {% do data.__setitem__( - field.field_name, - h.scheming_render_from_string(field.default_jinja2)) %} - {% elif field.default %} - {% do data.__setitem__(field.field_name, field.default) %} - {% endif %} - {% endif -%} - {# We pass pkg_name as the package_id because that's the only - variable available in this snippet #} - {%- snippet 'scheming/snippets/form_field.html', - field=field, - data=data, - errors=errors, - licenses=c.licenses, - entity_type='dataset', - object_type=dataset_type, - package_id=pkg_name - -%} - {%- endif -%} - {%- endfor -%} - + {% snippet 'scheming/snippets/render_fields.html', fields=schema.resource_fields, data=data, errors=errors, entity_type='dataset', object_type=dataset_type, set_fields_defaults=true %} {% endblock %} diff --git a/ckanext/scheming/templates/scheming/snippets/render_fields.html b/ckanext/scheming/templates/scheming/snippets/render_fields.html new file mode 100644 index 000000000..1cd331cbc --- /dev/null +++ b/ckanext/scheming/templates/scheming/snippets/render_fields.html @@ -0,0 +1,30 @@ +{#} + fields - a list of scheming field dictionaries + data - form data fields + errors - A dict of errors for the fields + entity_type - entity type + object_type - object type + set_fields_defaults - flag to set the default field values +{#} + +{% for field in fields if field.form_snippet is not none %} + {% if field.field_name not in data and set_fields_defaults %} + {# Set the field default value before rendering but only if + it doesn't already exist in data which would mean the form + has been submitted. #} + {% if field.default_jinja2 %} + {% do data.__setitem__(field.field_name, h.scheming_render_from_string(field.default_jinja2)) %} + {% elif field.default %} + {% do data.__setitem__(field.field_name, field.default) %} + {% endif %} + {% endif %} + + {% snippet 'scheming/snippets/form_field.html', + field=field, + data=data, + errors=errors, + licenses=licenses, + entity_type=entity_type, + object_type=object_type + %} +{% endfor %} diff --git a/ckanext/scheming/tests/test_arbitrary_schema.py b/ckanext/scheming/tests/test_arbitrary_schema.py new file mode 100644 index 000000000..f5428d41e --- /dev/null +++ b/ckanext/scheming/tests/test_arbitrary_schema.py @@ -0,0 +1,30 @@ +import pytest +from flask import render_template +from bs4 import BeautifulSoup + +from ckanext.scheming.helpers import scheming_get_arbitrary_schema + + +class TestArbitrarySchema: + def test_arbitrary_schema_structure(self): + schema = scheming_get_arbitrary_schema("ckanext_notifier") + + assert schema["scheming_version"] + assert schema["schema_id"] == "ckanext_notifier" + assert schema["about"] + assert isinstance(schema["fields"], list) + + @pytest.mark.usefixtures("with_request_context") + def test_render_arbitrary_schema(self, app): + schema = scheming_get_arbitrary_schema("ckanext_notifier") + + result = render_template( + "scheming/snippets/render_fields.html", + fields=schema["fields"], + data={}, + errors={}, + ) + + soup = BeautifulSoup(result) + + assert len(soup.select("div.form-group")) == 3 diff --git a/ckanext/scheming/tests/test_helpers.py b/ckanext/scheming/tests/test_helpers.py index 8aefe2a14..c066a8f67 100644 --- a/ckanext/scheming/tests/test_helpers.py +++ b/ckanext/scheming/tests/test_helpers.py @@ -6,6 +6,8 @@ from mock import patch, Mock import datetime + +import pytest import six from ckanext.scheming.helpers import ( @@ -15,6 +17,8 @@ scheming_get_presets, scheming_datastore_choices, scheming_display_json_value, + scheming_get_arbitrary_schema, + scheming_arbitrary_schemas, ) from ckanapi import NotFound @@ -27,9 +31,7 @@ def test_pass_through_gettext(self, _): assert "hello1" == scheming_language_text("hello") def test_only_one_language(self): - assert "hello" == scheming_language_text( - {"zh": "hello"}, prefer_lang="en" - ) + assert "hello" == scheming_language_text({"zh": "hello"}, prefer_lang="en") def test_matching_language(self): assert "hello" == scheming_language_text( @@ -42,7 +44,7 @@ def test_first_when_no_matching_language(self): ) def test_decodes_utf8(self): - assert u"\xa1Hola!" == scheming_language_text(six.b("\xc2\xa1Hola!")) + assert "\xa1Hola!" == scheming_language_text(six.b("\xc2\xa1Hola!")) @patch("ckanext.scheming.helpers.lang") def test_no_user_lang(self, lang): @@ -69,35 +71,35 @@ def test_scheming_get_presets(self): presets = scheming_get_presets() assert sorted( ( - u'title', - u'tag_string_autocomplete', - u'select', - u'resource_url_upload', - u'organization_url_upload', - u'resource_format_autocomplete', - u'multiple_select', - u'multiple_checkbox', - u'multiple_text', - u'date', - u'datetime', - u'datetime_tz', - u'dataset_slug', - u'dataset_organization', - u'json_object', - u'markdown', - u'radio', + "title", + "tag_string_autocomplete", + "select", + "resource_url_upload", + "organization_url_upload", + "resource_format_autocomplete", + "multiple_select", + "multiple_checkbox", + "multiple_text", + "date", + "datetime", + "datetime_tz", + "dataset_slug", + "dataset_organization", + "json_object", + "markdown", + "radio", ) ) == sorted(presets.keys()) def test_scheming_get_preset(self): - preset = scheming_get_preset(u"date") + preset = scheming_get_preset("date") assert sorted( ( - (u"display_snippet", u"date.html"), - (u"form_snippet", u"date.html"), + ("display_snippet", "date.html"), + ("form_snippet", "date.html"), ( - u"validators", - u"scheming_required isodate convert_to_json_if_date", + "validators", + "scheming_required isodate convert_to_json_if_date", ), ) ) == sorted(preset.items()) @@ -110,9 +112,7 @@ def test_no_choices_on_not_found(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices( - {"datastore_choices_resource": "not-found"} - ) + scheming_datastore_choices({"datastore_choices_resource": "not-found"}) == [] ) lc.action.datastore_search.assert_called_once() @@ -123,9 +123,7 @@ def test_no_choices_on_not_authorized(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices( - {"datastore_choices_resource": "not-allowed"} - ) + scheming_datastore_choices({"datastore_choices_resource": "not-allowed"}) == [] ) lc.action.datastore_search.assert_called_once() @@ -136,9 +134,7 @@ def test_no_choices_on_not_authorized(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices( - {"datastore_choices_resource": "not-allowed"} - ) + scheming_datastore_choices({"datastore_choices_resource": "not-allowed"}) == [] ) lc.action.datastore_search.assert_called_once() @@ -175,9 +171,10 @@ def test_call_with_all_params(self, LocalCKAN): "datastore_choices_resource": "all-params", "datastore_choices_limit": 5, "datastore_choices_columns": {"value": "a", "label": "b"}, - "datastore_additional_choices": - [{"value": "none", "label": "None"}, - {"value": "na", "label": "N/A"}] + "datastore_additional_choices": [ + {"value": "none", "label": "None"}, + {"value": "na", "label": "N/A"}, + ], } ) == [ {"value": "none", "label": "None"}, @@ -194,41 +191,56 @@ def test_call_with_all_params(self, LocalCKAN): class TestJSONHelpers(object): def test_display_json_value_default(self): - value = {"a": "b"} assert scheming_display_json_value(value) == '{\n "a": "b"\n}' def test_display_json_value_indent(self): - value = {"a": "b"} - assert ( - scheming_display_json_value(value, indent=4) - == '{\n "a": "b"\n}' - ) + assert scheming_display_json_value(value, indent=4) == '{\n "a": "b"\n}' def test_display_json_value_no_indent(self): - value = {"a": "b"} assert scheming_display_json_value(value, indent=None) == '{"a": "b"}' def test_display_json_value_keys_are_sorted(self): - value = {"c": "d", "a": "b"} if six.PY3: expected = '{\n "a": "b",\n "c": "d"\n}' else: expected = '{\n "a": "b", \n "c": "d"\n}' - assert ( - scheming_display_json_value(value, indent=4) == expected - ) + assert scheming_display_json_value(value, indent=4) == expected def test_display_json_value_json_error(self): - date = datetime.datetime.now() value = ("a", date) assert scheming_display_json_value(value) == ("a", date) + + +@pytest.mark.usefixtures("with_plugins") +class TestGetArbitrarySchemaHelper(object): + def test_get_all_arbitrary_schemas(self): + schemas = scheming_arbitrary_schemas() + + assert schemas + + @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") + def test_get_all_arbitrary_schemas_if_none(self): + schemas = scheming_arbitrary_schemas() + + assert not schemas + + def test_get_specific_schema(self): + schema = scheming_get_arbitrary_schema("ckanext_notifier") + + assert schema + + @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") + def test_get_specific_schema_if_none(self): + schema = scheming_get_arbitrary_schema("ckanext_notifier") + + assert not schema diff --git a/setup.py b/setup.py index 80b580125..c52bebfa6 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '3.0.0' +version = '3.0.1' setup( name='ckanext-scheming', @@ -37,6 +37,7 @@ scheming_datasets=ckanext.scheming.plugins:SchemingDatasetsPlugin scheming_groups=ckanext.scheming.plugins:SchemingGroupsPlugin scheming_organizations=ckanext.scheming.plugins:SchemingOrganizationsPlugin + scheming_arbitrary=ckanext.scheming.plugins:SchemingArbitraryPlugin scheming_nerf_index=ckanext.scheming.plugins:SchemingNerfIndexPlugin scheming_test_subclass=ckanext.scheming.tests.plugins:SchemingTestSubclass scheming_test_plugin=ckanext.scheming.tests.plugins:SchemingTestSchemaPlugin diff --git a/test.ini b/test.ini index 1dc6a6d3e..6fa564301 100644 --- a/test.ini +++ b/test.ini @@ -12,7 +12,7 @@ port = 5000 use = config:../../src/ckan/test-core.ini ckan.plugins = scheming_datasets scheming_groups scheming_organizations - scheming_test_plugin scheming_nerf_index + scheming_test_plugin scheming_nerf_index scheming_arbitrary scheming.dataset_schemas = ckanext.scheming:ckan_dataset.yaml ckanext.scheming.tests:test_schema.json ckanext.scheming.tests:test_subfields.yaml @@ -21,6 +21,7 @@ scheming.organization_schemas = ckanext.scheming:org_with_dept_id.json ckanext.scheming:custom_org_with_address.json scheming.group_schemas = ckanext.scheming:group_with_bookface.json ckanext.scheming:custom_group_with_status.json +scheming.arbitrary_schemas = ckanext.scheming:arbitrary_schema_example.yaml ckan.site_logo = /img/logo_64px_wide.png ckan.favicon = /images/icons/ckan.ico From 6654fe6bc3a4b1643c3c763d4231d85e32534dcb Mon Sep 17 00:00:00 2001 From: mutantsan Date: Mon, 12 Feb 2024 12:53:11 +0200 Subject: [PATCH 2/5] fix: return licenses to package_form template --- .../templates/scheming/package/snippets/package_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckanext/scheming/templates/scheming/package/snippets/package_form.html b/ckanext/scheming/templates/scheming/package/snippets/package_form.html index 5b01b67cb..ab973d5d4 100644 --- a/ckanext/scheming/templates/scheming/package/snippets/package_form.html +++ b/ckanext/scheming/templates/scheming/package/snippets/package_form.html @@ -69,7 +69,7 @@ {%- set fields = schema.dataset_fields -%} {%- endif -%} - {% snippet 'scheming/snippets/render_fields.html', fields=schema.dataset_fields, data=data, errors=errors, entity_type='dataset', object_type=dataset_type, set_fields_defaults=true %} + {% snippet 'scheming/snippets/render_fields.html', fields=schema.dataset_fields, data=data, errors=errors, licenses=c.licenses, entity_type='dataset', object_type=dataset_type, set_fields_defaults=true %} {%- if pages -%} From 49ebdc23030197eea9b7f671d520b386c3f80196 Mon Sep 17 00:00:00 2001 From: mutantsan Date: Mon, 12 Feb 2024 13:51:52 +0200 Subject: [PATCH 3/5] fix: fix tests --- .../scheming/tests/test_arbitrary_schema.py | 23 +++- ckanext/scheming/tests/test_helpers.py | 112 ++++++++---------- 2 files changed, 70 insertions(+), 65 deletions(-) diff --git a/ckanext/scheming/tests/test_arbitrary_schema.py b/ckanext/scheming/tests/test_arbitrary_schema.py index f5428d41e..c7e647fe2 100644 --- a/ckanext/scheming/tests/test_arbitrary_schema.py +++ b/ckanext/scheming/tests/test_arbitrary_schema.py @@ -2,12 +2,12 @@ from flask import render_template from bs4 import BeautifulSoup -from ckanext.scheming.helpers import scheming_get_arbitrary_schema +import ckan.plugins.toolkit as tk class TestArbitrarySchema: def test_arbitrary_schema_structure(self): - schema = scheming_get_arbitrary_schema("ckanext_notifier") + schema = tk.h.scheming_get_arbitrary_schema("ckanext_notifier") assert schema["scheming_version"] assert schema["schema_id"] == "ckanext_notifier" @@ -16,7 +16,7 @@ def test_arbitrary_schema_structure(self): @pytest.mark.usefixtures("with_request_context") def test_render_arbitrary_schema(self, app): - schema = scheming_get_arbitrary_schema("ckanext_notifier") + schema = tk.h.scheming_get_arbitrary_schema("ckanext_notifier") result = render_template( "scheming/snippets/render_fields.html", @@ -28,3 +28,20 @@ def test_render_arbitrary_schema(self, app): soup = BeautifulSoup(result) assert len(soup.select("div.form-group")) == 3 + + +@pytest.mark.usefixtures("with_plugins") +class TestGetArbitrarySchemaHelper: + def test_get_all_arbitrary_schemas(self): + assert tk.h.scheming_arbitrary_schemas() + + @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") + def test_get_all_arbitrary_schemas_if_none(self): + assert not tk.h.scheming_arbitrary_schemas() + + def test_get_specific_schema(self): + assert tk.h.scheming_get_arbitrary_schema("ckanext_notifier") + + @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") + def test_get_specific_schema_if_none(self): + assert not tk.h.scheming_get_arbitrary_schema("ckanext_notifier") diff --git a/ckanext/scheming/tests/test_helpers.py b/ckanext/scheming/tests/test_helpers.py index c066a8f67..8aefe2a14 100644 --- a/ckanext/scheming/tests/test_helpers.py +++ b/ckanext/scheming/tests/test_helpers.py @@ -6,8 +6,6 @@ from mock import patch, Mock import datetime - -import pytest import six from ckanext.scheming.helpers import ( @@ -17,8 +15,6 @@ scheming_get_presets, scheming_datastore_choices, scheming_display_json_value, - scheming_get_arbitrary_schema, - scheming_arbitrary_schemas, ) from ckanapi import NotFound @@ -31,7 +27,9 @@ def test_pass_through_gettext(self, _): assert "hello1" == scheming_language_text("hello") def test_only_one_language(self): - assert "hello" == scheming_language_text({"zh": "hello"}, prefer_lang="en") + assert "hello" == scheming_language_text( + {"zh": "hello"}, prefer_lang="en" + ) def test_matching_language(self): assert "hello" == scheming_language_text( @@ -44,7 +42,7 @@ def test_first_when_no_matching_language(self): ) def test_decodes_utf8(self): - assert "\xa1Hola!" == scheming_language_text(six.b("\xc2\xa1Hola!")) + assert u"\xa1Hola!" == scheming_language_text(six.b("\xc2\xa1Hola!")) @patch("ckanext.scheming.helpers.lang") def test_no_user_lang(self, lang): @@ -71,35 +69,35 @@ def test_scheming_get_presets(self): presets = scheming_get_presets() assert sorted( ( - "title", - "tag_string_autocomplete", - "select", - "resource_url_upload", - "organization_url_upload", - "resource_format_autocomplete", - "multiple_select", - "multiple_checkbox", - "multiple_text", - "date", - "datetime", - "datetime_tz", - "dataset_slug", - "dataset_organization", - "json_object", - "markdown", - "radio", + u'title', + u'tag_string_autocomplete', + u'select', + u'resource_url_upload', + u'organization_url_upload', + u'resource_format_autocomplete', + u'multiple_select', + u'multiple_checkbox', + u'multiple_text', + u'date', + u'datetime', + u'datetime_tz', + u'dataset_slug', + u'dataset_organization', + u'json_object', + u'markdown', + u'radio', ) ) == sorted(presets.keys()) def test_scheming_get_preset(self): - preset = scheming_get_preset("date") + preset = scheming_get_preset(u"date") assert sorted( ( - ("display_snippet", "date.html"), - ("form_snippet", "date.html"), + (u"display_snippet", u"date.html"), + (u"form_snippet", u"date.html"), ( - "validators", - "scheming_required isodate convert_to_json_if_date", + u"validators", + u"scheming_required isodate convert_to_json_if_date", ), ) ) == sorted(preset.items()) @@ -112,7 +110,9 @@ def test_no_choices_on_not_found(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices({"datastore_choices_resource": "not-found"}) + scheming_datastore_choices( + {"datastore_choices_resource": "not-found"} + ) == [] ) lc.action.datastore_search.assert_called_once() @@ -123,7 +123,9 @@ def test_no_choices_on_not_authorized(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices({"datastore_choices_resource": "not-allowed"}) + scheming_datastore_choices( + {"datastore_choices_resource": "not-allowed"} + ) == [] ) lc.action.datastore_search.assert_called_once() @@ -134,7 +136,9 @@ def test_no_choices_on_not_authorized(self, LocalCKAN): lc.action.datastore_search.side_effect = NotFound() LocalCKAN.return_value = lc assert ( - scheming_datastore_choices({"datastore_choices_resource": "not-allowed"}) + scheming_datastore_choices( + {"datastore_choices_resource": "not-allowed"} + ) == [] ) lc.action.datastore_search.assert_called_once() @@ -171,10 +175,9 @@ def test_call_with_all_params(self, LocalCKAN): "datastore_choices_resource": "all-params", "datastore_choices_limit": 5, "datastore_choices_columns": {"value": "a", "label": "b"}, - "datastore_additional_choices": [ - {"value": "none", "label": "None"}, - {"value": "na", "label": "N/A"}, - ], + "datastore_additional_choices": + [{"value": "none", "label": "None"}, + {"value": "na", "label": "N/A"}] } ) == [ {"value": "none", "label": "None"}, @@ -191,56 +194,41 @@ def test_call_with_all_params(self, LocalCKAN): class TestJSONHelpers(object): def test_display_json_value_default(self): + value = {"a": "b"} assert scheming_display_json_value(value) == '{\n "a": "b"\n}' def test_display_json_value_indent(self): + value = {"a": "b"} - assert scheming_display_json_value(value, indent=4) == '{\n "a": "b"\n}' + assert ( + scheming_display_json_value(value, indent=4) + == '{\n "a": "b"\n}' + ) def test_display_json_value_no_indent(self): + value = {"a": "b"} assert scheming_display_json_value(value, indent=None) == '{"a": "b"}' def test_display_json_value_keys_are_sorted(self): + value = {"c": "d", "a": "b"} if six.PY3: expected = '{\n "a": "b",\n "c": "d"\n}' else: expected = '{\n "a": "b", \n "c": "d"\n}' - assert scheming_display_json_value(value, indent=4) == expected + assert ( + scheming_display_json_value(value, indent=4) == expected + ) def test_display_json_value_json_error(self): + date = datetime.datetime.now() value = ("a", date) assert scheming_display_json_value(value) == ("a", date) - - -@pytest.mark.usefixtures("with_plugins") -class TestGetArbitrarySchemaHelper(object): - def test_get_all_arbitrary_schemas(self): - schemas = scheming_arbitrary_schemas() - - assert schemas - - @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") - def test_get_all_arbitrary_schemas_if_none(self): - schemas = scheming_arbitrary_schemas() - - assert not schemas - - def test_get_specific_schema(self): - schema = scheming_get_arbitrary_schema("ckanext_notifier") - - assert schema - - @pytest.mark.ckan_config("scheming.arbitrary_schemas", "") - def test_get_specific_schema_if_none(self): - schema = scheming_get_arbitrary_schema("ckanext_notifier") - - assert not schema From 54bf4269a6d3bc5cad0c89998875e009d1e59f20 Mon Sep 17 00:00:00 2001 From: mutantsan Date: Mon, 12 Feb 2024 15:08:05 +0200 Subject: [PATCH 4/5] doc: update readme --- README.md | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8773b3ef7..f9cf71331 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Table of contents: - [`group_type`](#group_type) - [`organization_type`](#organization_type) - [`fields`](#fields) + - [Arbitrary Schema Keys](#arbitrary-schema-keys) - [Field Keys](#field-keys) - [`field_name`](#field_name) - [`label`](#label) @@ -72,7 +73,7 @@ Set the schemas you want to use with configuration options: ```ini # Each of the plugins is optional depending on your use -ckan.plugins = scheming_datasets scheming_groups scheming_organizations +ckan.plugins = scheming_datasets scheming_groups scheming_organizations scheming_arbitrary # module-path:file to schemas being used scheming.dataset_schemas = ckanext.spatialx:spatialx_schema.yaml @@ -85,6 +86,7 @@ scheming.group_schemas = ckanext.scheming:group_with_bookface.json ckanext.myplugin:/etc/ckan/default/group_with_custom_fields.json scheming.organization_schemas = ckanext.scheming:org_with_dept_id.json ckanext.myplugin:org_with_custom_fields.json +scheming.arbitrary_schemas = ckanext.scheming:arbitrary_schema_example.yaml # # URLs may also be used, e.g: # @@ -100,6 +102,9 @@ scheming.dataset_fallback = false ## Schema Types With this plugin, you can customize the group, organization, and dataset entities in CKAN. Adding and enabling a schema will modify the forms used to update and create each entity, indicated by the respective `type` property at the root level. Such as `group_type`, `organization_type`, and `dataset_type`. Non-default types are supported properly as is indicated throughout the examples. +Moreover, `scheming_arbitrary` enables the definition and rendering of a custom form without being tied to a particular entity type. +The handling of a form submission must be implemented by the developer separately. + ## Example Schemas @@ -126,7 +131,9 @@ Organization schemas: * [Default organization schema with field modifications](ckanext/scheming/org_with_dept_id.json) * [Organization with custom type](ckanext/scheming/custom_org_with_address.json) +Arbitrary schemas: +* [Arbitrary schema example](ckanext/scheming/arbitrary_schema_example.yaml) ## Common Schema Keys @@ -231,6 +238,17 @@ fields: A single `fields` list replaces the `dataset_fields` and `resource_fields` schema properties doin dataset schemas. + + +## Arbitrary Schema Keys + +It closely resembles the group/organization schema, with the exception of a single field - `schema_id`. + +### `schema_id` + +The `schema_id` field serves as a unique identifier for any arbitrary schema, which is utilized within the codebase for retrieving the schema. + + ---------------- ## Field Keys From 507efcd869d44ee53a86f4b380afa87266916dd1 Mon Sep 17 00:00:00 2001 From: mutantsan Date: Tue, 16 Apr 2024 16:14:40 +0300 Subject: [PATCH 5/5] Fix: do not bump version, fix render_fields.html formatting --- .../scheming/snippets/render_fields.html | 48 +++++++++---------- setup.py | 2 +- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/ckanext/scheming/templates/scheming/snippets/render_fields.html b/ckanext/scheming/templates/scheming/snippets/render_fields.html index 1cd331cbc..8bf48f052 100644 --- a/ckanext/scheming/templates/scheming/snippets/render_fields.html +++ b/ckanext/scheming/templates/scheming/snippets/render_fields.html @@ -1,30 +1,30 @@ -{#} - fields - a list of scheming field dictionaries - data - form data fields - errors - A dict of errors for the fields - entity_type - entity type - object_type - object type - set_fields_defaults - flag to set the default field values +{# +fields - a list of scheming field dictionaries +data - form data fields +errors - A dict of errors for the fields +entity_type - entity type +object_type - object type +set_fields_defaults - flag to set the default field values {#} {% for field in fields if field.form_snippet is not none %} - {% if field.field_name not in data and set_fields_defaults %} - {# Set the field default value before rendering but only if - it doesn't already exist in data which would mean the form - has been submitted. #} - {% if field.default_jinja2 %} - {% do data.__setitem__(field.field_name, h.scheming_render_from_string(field.default_jinja2)) %} - {% elif field.default %} - {% do data.__setitem__(field.field_name, field.default) %} - {% endif %} + {% if field.field_name not in data and set_fields_defaults %} + {# Set the field default value before rendering but only if + it doesn't already exist in data which would mean the form + has been submitted. #} + {% if field.default_jinja2 %} + {% do data.__setitem__(field.field_name, h.scheming_render_from_string(field.default_jinja2)) %} + {% elif field.default %} + {% do data.__setitem__(field.field_name, field.default) %} {% endif %} + {% endif %} - {% snippet 'scheming/snippets/form_field.html', - field=field, - data=data, - errors=errors, - licenses=licenses, - entity_type=entity_type, - object_type=object_type - %} + {% snippet 'scheming/snippets/form_field.html', + field=field, + data=data, + errors=errors, + licenses=licenses, + entity_type=entity_type, + object_type=object_type + %} {% endfor %} diff --git a/setup.py b/setup.py index c52bebfa6..dc6332259 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -version = '3.0.1' +version = '3.0.0' setup( name='ckanext-scheming',