Customizing the DataStore Data Dictionary Form
Extensions can customize the Data Dictionary form, keys available and values
stored for each column using the
IDataDictionaryForm
interface.
- class ckanext.datastore.interfaces.IDataDictionaryForm
Allow data dictionary validation and per-plugin data storage by extending the datastore_create schema and adding values to fields returned from datastore_info
- update_datastore_create_schema(schema: Schema) Schema
Return a modified schema for handling field input in the data dictionary form and datastore_create parameters.
Validators are provided a plugin_data dict in the context that can be used to store per-field values. Top-level keys in this dict should match the field index, second-level keys should match the plugin name and values should be a dict with string keys storing data for that plugin.
e.g. a statistics plugin that needs to store per-column information might store this with plugin_data by inserting values like:
{0: {'statistics': {'minimum': 34, ...}, ...}, ...} # ^ the data stored for this field+plugin # ^ the name of the plugin #^ 0 for the first field passed in fields
Values not removed from field info by validation will be available in the field info dict returned from datastore_search and datastore_info
- update_datastore_info_field(field: dict[str, Any], plugin_data: dict[str, Any])
Return a modified version of the datastore_info field dict based on this field’s plugin_data to provide additional information to users and existing values for new form fields in the data dictionary page.
Let’s add five new keys with custom validation rules to the data dictionary fields.
With this plugin enabled each field in the Data Dictionary form will have an input for:
an integer value
a JSON object
a numeric value that can only be increased when edited
a “sticky” value that will not be removed if left blank
a secret value that will be stored but never displayed in the form.
First extend the form template to render the form inputs:
{% ckan_extends %}
{% block additional_fields %}
{{ form.input('fields__' ~ position ~ '__an_int',
label=_('An integer'), id='example-plugin-f' ~ position ~ 'an_int',
value=data.get('an_int', field.get('an_int', '')),
classes=['control-full'], error=errors.an_int) }}
{{ form.input('fields__' ~ position ~ '__json_obj',
label=_('JSON object'), id='example-plugin-f' ~ position ~ 'json_obj',
value=
data.json_obj if 'json_obj' in data else
h.dump_json(field['json_obj']) if 'json_obj' in field else '',
classes=['control-full'], error=errors.json_obj) }}
{{ form.input('fields__' ~ position ~ '__only_up',
label=_('Always increasing'), id='example-plugin-f' ~ position ~ 'only_up',
value=data.get('only_up', field.get('only_up', '')),
classes=['control-full'], error=errors.only_up) }}
{{ form.input('fields__' ~ position ~ '__sticky',
label=_('Sticky input'), id='example-plugin-f' ~ position ~ 'sticky',
value=data.get('sticky', field.get('sticky', '')),
classes=['control-full'], error=errors.sticky) }}
{{ form.input('fields__' ~ position ~ '__secret',
label=_('Secret (write-only)'),
id='example-plugin-f' ~ position ~ 'secret',
value='', classes=['control-full'],
error=errors.secret) }}
{% endblock %}
We use the form.input
macro to render the form fields. The name
of each field starts with fields__
and includes a position
index
because this block will be rendered once for every field in the data
dictionary.
The value for each input is set to either the value from data
the text
data passed when re-rendering a form containing errors, or field
the
json value (text, number, object etc.) currently stored in the data
dictionary when rendering a form for the first time.
The error for each field is set from errors
.
Next we create a plugin to apply the template and validation rules for each data dictionary field key.
# encoding: utf-8
from __future__ import annotations
from typing import Any, cast
from ckan.types import Schema, ValidatorFactory
from ckan.common import CKANConfig
from ckan.types import (
Context, FlattenDataDict, FlattenErrorDict, FlattenKey,
)
import json
from ckan.plugins.toolkit import (
Invalid, get_validator, add_template_directory, _, missing,
)
from ckan import plugins
from ckanext.datastore.interfaces import IDataDictionaryForm
class ExampleIDataDictionaryFormPlugin(plugins.SingletonPlugin):
plugins.implements(IDataDictionaryForm)
plugins.implements(plugins.IConfigurer)
# IConfigurer
def update_config(self, config: CKANConfig):
add_template_directory(config, 'templates')
# IDataDictionaryForm
def update_datastore_create_schema(self, schema: Schema):
ignore_empty = get_validator('ignore_empty')
int_validator = get_validator('int_validator')
unicode_only = get_validator('unicode_only')
datastore_default_current = get_validator('datastore_default_current')
to_datastore_plugin_data = cast(
ValidatorFactory, get_validator('to_datastore_plugin_data'))
to_eg_iddf = to_datastore_plugin_data('example_idatadictionaryform')
f = cast(Schema, schema['fields'])
f['an_int'] = [ignore_empty, int_validator, to_eg_iddf]
f['json_obj'] = [ignore_empty, json_obj, to_eg_iddf]
f['only_up'] = [
only_increasing, ignore_empty, int_validator, to_eg_iddf]
f['sticky'] = [
datastore_default_current, ignore_empty, unicode_only, to_eg_iddf]
# use different plugin_key so that value isn't removed
# when above fields are updated & value not exposed in
# datastore_info
f['secret'] = [
ignore_empty,
to_datastore_plugin_data('example_idatadictionaryform_secret')
]
return schema
def update_datastore_info_field(
self, field: dict[str, Any], plugin_data: dict[str, Any]):
# expose all our non-secret plugin data in the field
field.update(plugin_data.get('example_idatadictionaryform', {}))
return field
def json_obj(value: str | dict[str, Any]) -> dict[str, Any]:
'''accept only json objects i.e. dicts or "{...}"'''
try:
if isinstance(value, str):
value = json.loads(value)
else:
json.dumps(value)
if not isinstance(value, dict):
raise TypeError
return value
except (TypeError, ValueError):
raise Invalid(_('Not a JSON object'))
def only_increasing(
key: FlattenKey, data: FlattenDataDict,
errors: FlattenErrorDict, context: Context):
'''once set only accept new values larger than current value'''
value = data[key]
field_index = key[-2]
field_name = key[-1]
# current values for plugin_data are available as
# context['plugin_data'][field_index]['_current']
current = context['plugin_data'].get(field_index, {}).get(
'_current', {}).get('example_idatadictionaryform', {}).get(
field_name)
if current is None:
return
if value is not None and value != '' and value is not missing:
try:
if int(value) < current:
errors[key].append(
_('Value must be larger than %d') % current)
except ValueError:
return # allow int_validator to handle the error
else:
# keep current value when empty/missing
data[key] = current
In update_datastore_create_schema
the to_datastore_plugin_data
factory
generates a validator that will store our new keys as plugin data.
The string passed is used to group keys for this plugin to allow multiple
separate IDataDictionaryForm
plugins to store data for Data Dictionary
fields at the same time. It’s possible to use multiple groups from the same
plugin: here we use a different group for the secret
key because we want
to treat it differently.
In update_datastore_info_field
we can add keys stored as plugin data
to the fields
objects returned by datastore_info
. Here we add
everything but the secret
key. These values are also passed to the
form template above as field
.