Skip to content

Commit bb8452e

Browse files
authored
Merge pull request #189 from open-data/feature/handle-dataset-resource-validation
Handle Nested Resource Validation Errors
2 parents c7e89f5 + 1864452 commit bb8452e

File tree

16 files changed

+296
-29
lines changed

16 files changed

+296
-29
lines changed

changes/189.canada.feature

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Now handles nested resource validation errors. Creating, updating, and deleting a resource can catch and handle validation errors from other resources in the dataset.

ckan/lib/helpers.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1088,6 +1088,11 @@ def humanize_entity_type(entity_type: str, object_type: str,
10881088
u'search placeholder': _(u'Search {object_type}s...'),
10891089
u'you not member': _(u'You are not a member of any {object_type}s.'),
10901090
u'update label': _(u"Update {object_type}"),
1091+
# (canada fork only): handle all errors in resource actions
1092+
# TODO: upstream contrib??
1093+
'other errors': _('Errors in {object_type}'),
1094+
'other errors package': _('The {object_type} contains errors:'),
1095+
'other errors resources': _('The {object_type} contains invalid resources:')
10911096
}
10921097

10931098
type_label = object_type.replace(u"_", u" ").capitalize()

ckan/logic/action/__init__.py

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
import re
55
from copy import deepcopy
6-
from typing import Any, Mapping, cast
6+
# (canada fork only): handle all errors in resource actions
7+
# TODO: upstream contrib??
8+
from typing import Any, Mapping, cast, Tuple, Dict
79

810

911
from ckan.logic import NotFound
@@ -73,3 +75,77 @@ def prettify(field_name: str):
7375
else:
7476
summary[_(prettify(key))] = error[0]
7577
return summary
78+
79+
80+
# (canada fork only): handle all errors in resource actions
81+
# TODO: upstream contrib??
82+
def resource_validation_errors(
83+
error_dict: ErrorDict,
84+
action: str,
85+
pkg_dict: Dict[str, Any],
86+
resource_index: int = -1) -> Tuple[Dict[str, Any], str]:
87+
"""
88+
Returns a modified (error_dict, error_summary) with errors from the
89+
resource_index resource fields in error_dict, errors from the dataset
90+
fields under error_dict['dataset'] and errors from other resource
91+
fields under error_dict['resources'][resource_id].
92+
"""
93+
new_error_dict = dict(error_dict)
94+
# define out own error summaries so we can have
95+
# a more customized structure to the ErrorDict
96+
error_summaries = []
97+
if action == 'delete':
98+
# special case for deleting as there is no index
99+
# for a non-existent resource in the pkg_dict.
100+
current_res_error_dict = False
101+
else:
102+
current_res_error_dict = cast("list[ErrorDict]", error_dict['resources'])[resource_index]
103+
if current_res_error_dict:
104+
# if there are errors for the current resource
105+
# let's raise them to the user first.
106+
new_error_dict = dict(current_res_error_dict)
107+
if not current_res_error_dict and 'resources' in error_dict and isinstance(error_dict['resources'], list):
108+
# compile the other resource errors
109+
new_error_dict = {'resources': {}}
110+
for key, res_error_dict in enumerate(error_dict['resources']):
111+
if not res_error_dict:
112+
continue
113+
if key <= len(pkg_dict['resources']): # case for resource_delete
114+
errored_resource = pkg_dict['resources'][key]
115+
if errored_resource.get('id'):
116+
new_error_dict['resources'][errored_resource.get('id')] = res_error_dict
117+
if res_error_dict:
118+
if action == 'create' or action == 'update':
119+
error_summaries.append(
120+
_('Could not create or update resource because another resource in '
121+
'this dataset has errors: {}').format(errored_resource['id']))
122+
elif action == 'delete':
123+
error_summaries.append(
124+
_('Could not delete resource because another resource in '
125+
'this dataset has errors: {}').format(errored_resource['id']))
126+
elif action == 'reorder':
127+
error_summaries.append(
128+
_('Could not reorder resources because a resource in '
129+
'this dataset has errors: {}').format(errored_resource['id']))
130+
if error_dict:
131+
# compile the dataset errors
132+
for key, errors in error_dict.items():
133+
if key == 'resources':
134+
continue
135+
if 'dataset' not in new_error_dict:
136+
new_error_dict['dataset'] = {}
137+
new_error_dict['dataset'][key] = errors
138+
if 'dataset' in new_error_dict:
139+
if action == 'create' or action == 'update':
140+
error_summaries.append(
141+
_('Could not create or update resource because '
142+
'the dataset has errors'))
143+
elif action == 'delete':
144+
error_summaries.append(
145+
_('Could not delete resource because '
146+
'the dataset has errors'))
147+
elif action == 'reorder':
148+
error_summaries.append(
149+
_('Could not reorder resources because '
150+
'the dataset has errors'))
151+
return new_error_dict, _('; ').join(error_summaries)

ckan/logic/action/create.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import ckan.authz as authz
3434
import ckan.model
3535

36+
# (canada fork only): handle all errors in resource actions
37+
# TODO: upstream contrib??
38+
from . import resource_validation_errors
3639
from ckan.common import _
3740
from ckan.types import Context, DataDict, ErrorDict, Schema
3841

@@ -329,11 +332,11 @@ def resource_create(context: Context,
329332
_get_action('package_update')(context, pkg_dict)
330333
context.pop('defer_commit')
331334
except ValidationError as e:
332-
try:
333-
error_dict = cast("list[ErrorDict]", e.error_dict['resources'])[-1]
334-
except (KeyError, IndexError):
335-
error_dict = e.error_dict
336-
raise ValidationError(error_dict)
335+
# (canada fork only): handle all errors in resource actions
336+
# TODO: upstream contrib??
337+
error_dict, error_summary = resource_validation_errors(
338+
e.error_dict, action='create', pkg_dict=pkg_dict)
339+
raise ValidationError(error_dict, error_summary=error_summary)
337340

338341
# Get out resource_id resource from model as it will not appear in
339342
# package_show until after commit

ckan/logic/action/delete.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@
1919
from ckan.lib.navl.dictization_functions import validate
2020
from ckan.model.follower import ModelFollowingModel
2121

22+
# (canada fork only): handle all errors in resource actions
23+
# TODO: upstream contrib??
24+
from . import resource_validation_errors
2225
from ckan.common import _
2326
from ckan.types import Context, DataDict, ErrorDict, Schema
2427

@@ -195,8 +198,11 @@ def resource_delete(context: Context, data_dict: DataDict) -> ActionResult.Resou
195198
try:
196199
pkg_dict = _get_action('package_update')(context, pkg_dict)
197200
except ValidationError as e:
198-
errors = cast("list[ErrorDict]", e.error_dict['resources'])[-1]
199-
raise ValidationError(errors)
201+
# (canada fork only): handle all errors in resource actions
202+
# TODO: upstream contrib??
203+
error_dict, error_summary = resource_validation_errors(
204+
e.error_dict, action='delete', pkg_dict=pkg_dict)
205+
raise ValidationError(error_dict, error_summary=error_summary)
200206

201207
for plugin in plugins.PluginImplementations(plugins.IResourceController):
202208
plugin.after_resource_delete(context, pkg_dict.get('resources', []))

ckan/logic/action/update.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
#TODO: upstream contrib!!
2929
import ckan.lib.search.jobs as search_jobs
3030

31+
# (canada fork only): handle all errors in resource actions
32+
# TODO: upstream contrib??
33+
from . import resource_validation_errors
3134
from ckan.common import _, config
3235
from ckan.types import Context, DataDict, ErrorDict
3336

@@ -114,11 +117,11 @@ def resource_update(context: Context, data_dict: DataDict) -> ActionResult.Resou
114117
context['use_cache'] = False
115118
updated_pkg_dict = _get_action('package_update')(context, pkg_dict)
116119
except ValidationError as e:
117-
try:
118-
error_dict = cast("list[ErrorDict]", e.error_dict['resources'])[n]
119-
except (KeyError, IndexError):
120-
error_dict = e.error_dict
121-
raise ValidationError(error_dict)
120+
# (canada fork only): handle all errors in resource actions
121+
# TODO: upstream contrib??
122+
error_dict, error_summary = resource_validation_errors(
123+
e.error_dict, action='update', pkg_dict=pkg_dict, resource_index=n)
124+
raise ValidationError(error_dict, error_summary=error_summary)
122125

123126
resource = _get_action('resource_show')(context, {'id': id})
124127

@@ -579,9 +582,18 @@ def package_resource_reorder(
579582
package_dict['resources'] = new_resources
580583

581584
_check_access('package_resource_reorder', context, package_dict)
582-
_get_action('package_update')(context, package_dict)
585+
# (canada fork only): handle all errors in resource actions
586+
# TODO: upstream contrib??
587+
try:
588+
_get_action('package_update')(context, package_dict)
589+
except ValidationError as e:
590+
error_dict, error_summary = resource_validation_errors(
591+
e.error_dict, action='reorder', pkg_dict=package_dict)
592+
return {'id': id, 'order': [resource['id'] for resource in new_resources],
593+
'errors': error_dict, 'error_summary': error_summary}
583594

584-
return {'id': id, 'order': [resource['id'] for resource in new_resources]}
595+
return {'id': id, 'order': [resource['id'] for resource in new_resources],
596+
'errors': None, 'error_summary': None}
585597

586598

587599
def _update_package_relationship(

ckan/public/base/javascript/modules/resource-reorder.js

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ this.ckan.module('resource-reorder', function($) {
3030
'<i class="fa fa-spinner fa-spin"></i>',
3131
'<span></span>',
3232
'</span>'
33+
].join('\n'),
34+
// (canada fork only): handle all errors in resource actions
35+
// TODO: upstream contrib??
36+
errors: [
37+
'<div class="error-explanation alert alert-danger">',
38+
'<h3></h3>',
39+
'<p></p>',
40+
'</div>'
3341
].join('\n')
3442
},
3543
is_reordering: false,
@@ -47,6 +55,13 @@ this.ckan.module('resource-reorder', function($) {
4755
.insertBefore(this.el)
4856
.hide();
4957

58+
// (canada fork only): handle all errors in resource actions
59+
// TODO: upstream contrib??
60+
this.html_errors = $(this.template.errors)
61+
.insertBefore(this.el)
62+
.hide();
63+
$(this.html_errors).find('h3').text(this._('Errors in dataset'));
64+
5065
this.html_help_text = $(this.template.help_text)
5166
.text(helpText)
5267
.insertBefore(this.el)
@@ -126,7 +141,13 @@ this.ckan.module('resource-reorder', function($) {
126141
module.sandbox.client.call('POST', 'package_resource_reorder', {
127142
id: module.options.id,
128143
order: order
129-
}, function() {
144+
}, function(data) {
145+
// (canada fork only): handle all errors in resource actions
146+
// TODO: upstream contrib??
147+
if( typeof data.result !== 'undefined' && typeof data.result.error_summary !== 'undefined' ){
148+
$(module.html_errors).find('p').text(data.result.error_summary);
149+
$(module.html_errors).show();
150+
}
130151
module.html_saving.hide();
131152
$('.save, .cancel', module.html_form_actions).removeClass('disabled');
132153
module.cache = module.el.html();

ckan/templates/macros/form.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
{% from "macros/form/prepend.html" import prepend %}
1414
{% from "macros/form/custom.html" import custom %}
1515
{% from "macros/form/errors.html" import errors %}
16+
{# (canada fork only): handle all errors in resource actions #}
17+
{# TODO: upstream contrib?? #}
18+
{% from "macros/form/resource_validation_errors.html" import resource_validation_errors %}
1619
{% from "macros/form/info.html" import info %}
1720
{% from "macros/form/hidden.html" import hidden %}
1821
{% from "macros/form/hidden_from_list.html" import hidden_from_list %}
@@ -29,6 +32,9 @@
2932
{% set prepend = prepend %}
3033
{% set custom = custom %}
3134
{% set errors = errors %}
35+
{# (canada fork only): handle all errors in resource actions #}
36+
{# TODO: upstream contrib?? #}
37+
{% set resource_validation_errors = resource_validation_errors %}
3238
{% set info = info %}
3339
{% set hidden = hidden %}
3440
{% set hidden_from_list = hidden_from_list %}

ckan/templates/macros/form/errors.html

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@
1212

1313
#}
1414

15-
{% macro errors(errors={}, type="error", classes=[]) %}
15+
{# (canada fork only): error -> danger BS version #}
16+
{% macro errors(errors={}, type="danger", classes=[]) %}
1617
{% if errors %}
1718
<div class="error-explanation alert alert-{{ type }}{{ " " ~ classes | join(' ') }}">
18-
<p>{{ _('The form contains invalid entries:') }}</p>
19-
<ul>
20-
{% for key, error in errors.items() %}
21-
<li data-field-label="{{ key }}">{% if key %}{{ key }}: {% endif %}{{ error }}</li>
22-
{% endfor %}
23-
</ul>
19+
<p>{{ _('The form contains invalid entries:') }}</p>
20+
<ul>
21+
{% for key, error in errors.items() %}
22+
<li data-field-label="{{ key }}">{% if key %}{{ key }}: {% endif %}{{ error }}</li>
23+
{% endfor %}
24+
</ul>
2425
</div>
2526
{% endif %}
26-
{% endmacro %}
27+
{% endmacro %}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
{# (canada fork only): handle all errors in resource actions #}
2+
{# TODO: upstream contrib?? #}
3+
4+
{#
5+
Builds a list of errors for the current form.
6+
7+
errors - A dict of object/field/message pairs from the resource_validation_errors method.
8+
'resources' and 'dataset' keys.
9+
type - The alert-* class that should be applied (default: "danger")
10+
classes - A list of classes to apply to the wrapper (default: [])
11+
pkg_name - String; The name or ID of the package.
12+
dataset_type - String; The type of package
13+
h - The template helpers object as Jinja macros are scoped.
14+
15+
Example:
16+
17+
{% import 'macros/form.html' as form %}
18+
{{ form.resource_validation_errors(errors=resource_validation_errors, type="warning", pkg_name=pkg_dict.id, h=h) }}
19+
20+
#}
21+
22+
23+
{# (canada fork only): error -> danger BS version #}
24+
{% macro resource_validation_errors(errors={}, type="danger", classes=[], pkg_name='', dataset_type='dataset', h={}) %}
25+
{% if errors %}
26+
<div class="error-explanation alert alert-{{ type }}{{ " " ~ classes | join(' ') }}">
27+
<h3>{{ h.humanize_entity_type('package', dataset_type, 'other errors') or _('Errors in dataset') }}</h3>
28+
{% if 'dataset' in errors and errors.dataset %}
29+
<p>{{ h.humanize_entity_type('package', dataset_type, 'other errors package') or _('The dataset contains errors:') }}</p>
30+
<ul>
31+
{% for field_id, field_errs in errors.dataset.items() %}
32+
<li data-field-label="{{ field_id }}">{% if field_id %}{{ field_id }}{{_(':')}} {% endif %}{{ field_errs|join(', ') }}</li>
33+
{% endfor %}
34+
</ul>
35+
{% endif %}
36+
{% if 'resources' in errors and errors.resources %}
37+
<p>{{ h.humanize_entity_type('package', dataset_type, 'other errors resources') or _('The dataset contains invalid resources:') }}</p>
38+
<ul>
39+
{% for res_id, errs in errors.resources.items() %}
40+
<li><a href="{{ h.url_for('resource.read', id=pkg_name, resource_id=res_id) }}" target="_blank">{{ res_id }}</a></li>
41+
<ul>
42+
{% for field_id, field_errs in errs.items() %}
43+
<li data-field-label="{{ field_id }}">{% if field_id %}{{ field_id }}{{_(':')}} {% endif %}{{ field_errs|join(', ') }}</li>
44+
{% endfor %}
45+
</ul>
46+
{% endfor %}
47+
</ul>
48+
{% endif %}
49+
</div>
50+
{% endif %}
51+
{% endmacro %}

0 commit comments

Comments
 (0)