Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/189.canada.feature
Original file line number Diff line number Diff line change
@@ -0,0 +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.
77 changes: 76 additions & 1 deletion ckan/logic/action/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@

import re
from copy import deepcopy
from typing import Any, Mapping, cast
# (canada fork only): handle all errors in resource actions
# TODO: upstream contrib??
from typing import Any, Mapping, cast, Tuple, Dict


from ckan.logic import NotFound
Expand Down Expand Up @@ -73,3 +75,76 @@ def prettify(field_name: str):
else:
summary[_(prettify(key))] = error[0]
return summary


# (canada fork only): handle all errors in resource actions
# TODO: upstream contrib??
def resource_validation_errors(
error_dict: ErrorDict,
action: str,
pkg_dict: Dict[str, Any],
resource_index: int = -1) -> Tuple[Dict[str, Any], str]:
"""
Checks through the error_dict to find all errors in
the Dataset and its Resources.
"""
new_error_dict = dict(error_dict)
# define out own error summaries so we can have
# a more customized structure to the ErrorDict
error_summaries = []
try:
if action == 'delete':
# special case for deleting as there is no index
# for a non-existent resource in the pkg_dict.
current_res_error_dict = False
else:
current_res_error_dict = cast("list[ErrorDict]", error_dict['resources'])[resource_index]
if current_res_error_dict:
# if there are errors for the current resource
# let's raise them to the user first.
new_error_dict = current_res_error_dict
if not current_res_error_dict and 'resources' in error_dict and isinstance(error_dict['resources'], list):
# compile the other resource errors
new_error_dict = {'resources': {}}
for key, res_error_dict in enumerate(error_dict['resources']):
if key <= len(pkg_dict['resources']):
errored_resource = pkg_dict['resources'][key]
if errored_resource.get('id'):
new_error_dict['resources'][errored_resource.get('id')] = res_error_dict
if res_error_dict:
if action == 'create' or action == 'update':
error_summaries.append(
_('Could not create or update resource because another resource in '
'this dataset has errors: {}').format(errored_resource['id']))
elif action == 'delete':
error_summaries.append(
_('Could not delete resource because another resource in '
'this dataset has errors: {}').format(errored_resource['id']))
elif action == 'reorder':
error_summaries.append(
_('Could not reorder resources because a resource in '
'this dataset has errors: {}').format(errored_resource['id']))
if error_dict:
# compile the dataset errors
for key, errors in error_dict.items():
if key == 'resources':
continue
if 'dataset' not in new_error_dict:
new_error_dict['dataset'] = {}
new_error_dict['dataset'][key] = errors
if 'dataset' in new_error_dict:
if action == 'create' or action == 'update':
error_summaries.append(
_('Could not create or update resource because '
'the dataset has errors'))
elif action == 'delete':
error_summaries.append(
_('Could not delete resource because '
'the dataset has errors'))
elif action == 'reorder':
error_summaries.append(
_('Could not reorder resources because '
'the dataset has errors'))
except (KeyError, IndexError):
new_error_dict = dict(error_dict)
return new_error_dict, '; '.join(error_summaries)
13 changes: 8 additions & 5 deletions ckan/logic/action/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
import ckan.authz as authz
import ckan.model

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

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

# Get out resource_id resource from model as it will not appear in
# package_show until after commit
Expand Down
10 changes: 8 additions & 2 deletions ckan/logic/action/delete.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
from ckan.lib.navl.dictization_functions import validate
from ckan.model.follower import ModelFollowingModel

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

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

for plugin in plugins.PluginImplementations(plugins.IResourceController):
plugin.after_resource_delete(context, pkg_dict.get('resources', []))
Expand Down
26 changes: 19 additions & 7 deletions ckan/logic/action/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
#TODO: upstream contrib!!
import ckan.lib.search.jobs as search_jobs

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

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

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

Expand Down Expand Up @@ -579,9 +582,18 @@ def package_resource_reorder(
package_dict['resources'] = new_resources

_check_access('package_resource_reorder', context, package_dict)
_get_action('package_update')(context, package_dict)
# (canada fork only): handle all errors in resource actions
# TODO: upstream contrib??
try:
_get_action('package_update')(context, package_dict)
except ValidationError as e:
error_dict, error_summary = resource_validation_errors(
e.error_dict, action='reorder', pkg_dict=package_dict)
return {'id': id, 'order': [resource['id'] for resource in new_resources],
'errors': error_dict, 'error_summary': error_summary}

return {'id': id, 'order': [resource['id'] for resource in new_resources]}
return {'id': id, 'order': [resource['id'] for resource in new_resources],
'errors': None, 'error_summary': None}


def _update_package_relationship(
Expand Down
23 changes: 22 additions & 1 deletion ckan/public/base/javascript/modules/resource-reorder.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ this.ckan.module('resource-reorder', function($) {
'<i class="fa fa-spinner fa-spin"></i>',
'<span></span>',
'</span>'
].join('\n'),
// (canada fork only): handle all errors in resource actions
// TODO: upstream contrib??
errors: [
'<div class="error-explanation alert alert-danger">',
'<h3></h3>',
'<p></p>',
'</div>'
].join('\n')
},
is_reordering: false,
Expand All @@ -47,6 +55,13 @@ this.ckan.module('resource-reorder', function($) {
.insertBefore(this.el)
.hide();

// (canada fork only): handle all errors in resource actions
// TODO: upstream contrib??
this.html_errors = $(this.template.errors)
.insertBefore(this.el)
.hide();
$(this.html_errors).find('h3').text(this._('Errors in dataset'));

this.html_help_text = $(this.template.help_text)
.text(helpText)
.insertBefore(this.el)
Expand Down Expand Up @@ -126,7 +141,13 @@ this.ckan.module('resource-reorder', function($) {
module.sandbox.client.call('POST', 'package_resource_reorder', {
id: module.options.id,
order: order
}, function() {
}, function(data) {
// (canada fork only): handle all errors in resource actions
// TODO: upstream contrib??
if( typeof data.result !== 'undefined' && typeof data.result.error_summary !== 'undefined' ){
$(module.html_errors).find('p').text(data.result.error_summary);
$(module.html_errors).show();
}
module.html_saving.hide();
$('.save, .cancel', module.html_form_actions).removeClass('disabled');
module.cache = module.el.html();
Expand Down
6 changes: 6 additions & 0 deletions ckan/templates/macros/form.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
{% from "macros/form/prepend.html" import prepend %}
{% from "macros/form/custom.html" import custom %}
{% from "macros/form/errors.html" import errors %}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% from "macros/form/dataset_errors.html" import dataset_errors %}
{% from "macros/form/info.html" import info %}
{% from "macros/form/hidden.html" import hidden %}
{% from "macros/form/hidden_from_list.html" import hidden_from_list %}
Expand All @@ -29,6 +32,9 @@
{% set prepend = prepend %}
{% set custom = custom %}
{% set errors = errors %}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% set dataset_errors = dataset_errors %}
{% set info = info %}
{% set hidden = hidden %}
{% set hidden_from_list = hidden_from_list %}
Expand Down
51 changes: 51 additions & 0 deletions ckan/templates/macros/form/dataset_errors.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}

{#
Builds a list of errors for the current form.

errors - A dict of field/message pairs.
type - The alert-* class that should be applied (default: "error")
classes - A list of classes to apply to the wrapper (default: [])

Example:

{% import 'macros/form.html' as form %}
{{ form.errors(error_summary, type="warning") }}

#}


{# (canada fork only): error -> danger BS version #}
{% macro dataset_errors(errors={}, type="danger", classes=[], pkg_name='', helpers={}) %}
{% if errors %}
<div class="error-explanation alert alert-{{ type }}{{ " " ~ classes | join(' ') }}">
<h3>{{_('Errors in dataset')}}</h3>
{% if 'dataset' in errors and errors.dataset %}
{% set dataset_link = helpers.url_for('dataset.read', id=pkg_name) %}
<p>{{ _('The <a href="{}" target="_blank">dataset</a> contains errors:').format(dataset_link) }}</p>
<ul>
{% for field_id, field_errs in errors.dataset.items() %}
<li data-field-label="{{ field_id }}">{% if field_id %}{{ field_id }}{{_(':')}} {% endif %}{{ field_errs|join(', ') }}</li>
{% endfor %}
</ul>
{% endif %}
{% if 'resources' in errors and errors.resources %}
<p>{{ _('The dataset contains invalid resources:') }}</p>
<ul>
{% for res_id, errs in errors.resources.items() %}
{% if not errs %}
{% continue %}
{% endif %}
<li><a href="{{ helpers.url_for('resource.read', id=pkg_name, resource_id=res_id) }}" target="_blank">{{ _('Resource') ~ ' ' ~ loop.index }}</a></li>
<ul>
{% for field_id, field_errs in errs.items() %}
<li data-field-label="{{ field_id }}">{% if field_id %}{{ field_id }}{{_(':')}} {% endif %}{{ field_errs|join(', ') }}</li>
{% endfor %}
</ul>
{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
{% endmacro %}
17 changes: 9 additions & 8 deletions ckan/templates/macros/form/errors.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,16 @@

#}

{% macro errors(errors={}, type="error", classes=[]) %}
{# (canada fork only): error -> danger BS version #}
{% macro errors(errors={}, type="danger", classes=[]) %}
{% if errors %}
<div class="error-explanation alert alert-{{ type }}{{ " " ~ classes | join(' ') }}">
<p>{{ _('The form contains invalid entries:') }}</p>
<ul>
{% for key, error in errors.items() %}
<li data-field-label="{{ key }}">{% if key %}{{ key }}: {% endif %}{{ error }}</li>
{% endfor %}
</ul>
<p>{{ _('The form contains invalid entries:') }}</p>
<ul>
{% for key, error in errors.items() %}
<li data-field-label="{{ key }}">{% if key %}{{ key }}: {% endif %}{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% endmacro %}
{% endmacro %}
15 changes: 15 additions & 0 deletions ckan/templates/package/confirm_delete_resource.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
{% extends "page.html" %}

{# (canada fork only): handle validation errors #}
{# TODO: upstream contrib?? #}
{% import 'macros/form.html' as form %}

{% block subtitle %}{{ _("Confirm Delete") }}{% endblock %}

{% block maintag %}<div class="row" role="main">{% endblock %}
Expand All @@ -8,6 +12,17 @@
<section class="module col-md-6 col-md-offset-3">
<div class="module-content">
{% block form %}
{# (canada fork only): handle validation errors #}
{# TODO: upstream contrib?? #}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% block errors %}
{% if dataset_errors %}
{{ form.dataset_errors(errors=dataset_errors, pkg_name=pkg_id, helpers=h) }}
{% else %}
{{ form.errors(errors=errors) }}
{% endif %}
{% endblock %}
<p>{{ _('Are you sure you want to delete resource - {name}?').format(name=h.resource_display_name(resource_dict)) }}</p>
<p class="form-actions">
<form id='confirm-resource-delete-form' action="{% url_for 'resource.delete', resource_id=resource_dict.id, id=pkg_id %}" method="post">
Expand Down
4 changes: 3 additions & 1 deletion ckan/templates/package/new_resource.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
{% endif %}
{% endblock %}

{% block form %}{% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, include_metadata=false, pkg_name=pkg_name, stage=stage, dataset_type=dataset_type %}{% endblock %}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% block form %}{% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, dataset_errors=dataset_errors, include_metadata=false, pkg_name=pkg_name, stage=stage, dataset_type=dataset_type %}{% endblock %}

{% block secondary_content %}
{% snippet 'package/snippets/resource_help.html' %}
Expand Down
4 changes: 3 additions & 1 deletion ckan/templates/package/new_resource_not_draft.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
{% endblock %}

{% block form %}
{% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, include_metadata=false, pkg_name=pkg_name, stage=stage, dataset_type=dataset_type %}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, dataset_errors=dataset_errors, include_metadata=false, pkg_name=pkg_name, stage=stage, dataset_type=dataset_type %}
{% endblock %}

{% block content_primary_nav %}
Expand Down
3 changes: 3 additions & 0 deletions ckan/templates/package/resource_edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
{% block subtitle %}{{ _('Edit') }} {{ g.template_title_delimiter }} {{ h.resource_display_name(res) }} {{ g.template_title_delimiter }} {{ h.dataset_display_name(pkg) }}{% endblock %}

{% block form %}
{# (canada fork only): handle all errors in resource actions #}
{# TODO: upstream contrib?? #}
{% snippet 'package/snippets/resource_edit_form.html',
data=data,
errors=errors,
error_summary=error_summary,
dataset_errors=dataset_errors,
pkg_name=pkg.name,
form_action=form_action,
resource_form_snippet=resource_form_snippet,
Expand Down
Loading