Skip to content

Commit

Permalink
Add rudimentary UIs for metadata records
Browse files Browse the repository at this point in the history
  • Loading branch information
mark-saeon committed Dec 13, 2018
1 parent 2717c6a commit 1654b56
Show file tree
Hide file tree
Showing 22 changed files with 601 additions and 2 deletions.
262 changes: 262 additions & 0 deletions ckanext/metadata/controllers/metadata_record.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
# encoding: utf-8

import ckan.plugins.toolkit as tk
import ckan.model as model
import ckan.lib.helpers as helpers
import ckan.authz as authz
from ckan.logic import clean_dict, tuplize_dict, parse_params
import ckan.lib.navl.dictization_functions as dict_fns


class MetadataRecordController(tk.BaseController):

# Note: URLs must be constructed using metadata_record.id rather than metadata_record.name,
# because name can be mapped from a metadata JSON element which we cannot rely on to be
# URL safe; e.g. if name gets the DOI it will contain a '/'.

@staticmethod
def _set_containers_on_context(organization_id, metadata_collection_id):
context = {'model': model, 'session': model.Session, 'user': tk.c.user}

if organization_id and not tk.c.organization:
data_dict = {'id': organization_id}
try:
tk.c.organization = tk.get_action('organization_show')(context, data_dict)
except tk.ObjectNotFound:
tk.abort(404, tk._('Organization not found'))
except tk.NotAuthorized:
tk.abort(403, tk._('Not authorized to see this page'))

if metadata_collection_id and not tk.c.metadata_collection:
data_dict = {'id': metadata_collection_id}
try:
tk.c.metadata_collection = tk.get_action('metadata_collection_show')(context, data_dict)
except tk.ObjectNotFound:
tk.abort(404, tk._('Metadata collection not found'))
except tk.NotAuthorized:
tk.abort(403, tk._('Not authorized to see this page'))

if not organization_id:
tk.abort(400, tk._('Organization not specified'))
org = tk.c.organization
if tk.c.metadata_collection['organization_id'] not in (org['id'], org['name']):
tk.abort(400, tk._('Metadata collection does not belong to the specified organization'))

def index(self, organization_id=None, metadata_collection_id=None):
self._set_containers_on_context(organization_id, metadata_collection_id)

page = tk.h.get_page_number(tk.request.params) or 1
items_per_page = 21

context = {'model': model, 'session': model.Session,
'user': tk.c.user, 'for_view': True}

q = tk.c.q = tk.request.params.get('q', '')
sort_by = tk.c.sort_by_selected = tk.request.params.get('sort')
try:
tk.check_access('site_read', context)
tk.check_access('metadata_record_list', context)
except tk.NotAuthorized:
tk.abort(403, tk._('Not authorized to see this page'))

if tk.c.userobj:
context['user_id'] = tk.c.userobj.id
context['user_is_admin'] = tk.c.userobj.sysadmin

try:
data_dict_global_results = {
'owner_org': organization_id,
'metadata_collection_id': metadata_collection_id,
'all_fields': False,
'q': q,
'sort': sort_by,
'type': 'metadata_record',
}
global_results = tk.get_action('metadata_record_list')(context, data_dict_global_results)
except tk.ValidationError as e:
if e.error_dict and e.error_dict.get('message'):
msg = e.error_dict['message']
else:
msg = str(e)
tk.h.flash_error(msg)
tk.c.page = helpers.Page([], 0)
return tk.render('metadata_record/index.html')

data_dict_page_results = {
'owner_org': organization_id,
'metadata_collection_id': metadata_collection_id,
'all_fields': True,
'q': q,
'sort': sort_by,
'limit': items_per_page,
'offset': items_per_page * (page - 1),
}
page_results = tk.get_action('metadata_record_list')(context, data_dict_page_results)

tk.c.page = helpers.Page(
collection=global_results,
page=page,
url=tk.h.pager_url,
items_per_page=items_per_page,
)

tk.c.page.items = page_results
return tk.render('metadata_record/index.html')

def new(self, data=None, errors=None, error_summary=None, organization_id=None, metadata_collection_id=None):
self._set_containers_on_context(organization_id, metadata_collection_id)

context = {'model': model, 'session': model.Session, 'user': tk.c.user,
'save': 'save' in tk.request.params}
try:
tk.check_access('metadata_record_create', context)
except tk.NotAuthorized:
tk.abort(403, tk._('Unauthorized to create a metadata record'))

if context['save'] and not data and tk.request.method == 'POST':
return self._save_new(context)

data = data or {}
errors = errors or {}
error_summary = error_summary or {}
vars = {'data': data, 'errors': errors, 'error_summary': error_summary, 'action': 'new',
'metadata_standard_lookup_list': self._metadata_standard_lookup_list(),
'infrastructure_lookup_list': self._infrastructure_lookup_list()}

tk.c.is_sysadmin = authz.is_sysadmin(tk.c.user)
tk.c.form = tk.render('metadata_record/edit_form.html', extra_vars=vars)
return tk.render('metadata_record/new.html')

def edit(self, id, data=None, errors=None, error_summary=None, organization_id=None, metadata_collection_id=None):
self._set_containers_on_context(organization_id, metadata_collection_id)

context = {'model': model, 'session': model.Session, 'user': tk.c.user,
'save': 'save' in tk.request.params, 'for_edit': True}
data_dict = {'id': id}

if context['save'] and not data and tk.request.method == 'POST':
return self._save_edit(id, context)

try:
old_data = tk.get_action('metadata_record_show')(context, data_dict)
data = data or old_data
except (tk.ObjectNotFound, tk.NotAuthorized):
tk.abort(404, tk._('Metadata record not found'))

tk.c.metadata_record = old_data
try:
tk.check_access('metadata_record_update', context)
except tk.NotAuthorized:
tk.abort(403, tk._('User %r not authorized to edit %s') % (tk.c.user, id))

errors = errors or {}
vars = {'data': data, 'errors': errors, 'error_summary': error_summary, 'action': 'edit',
'metadata_standard_lookup_list': self._metadata_standard_lookup_list(),
'infrastructure_lookup_list': self._infrastructure_lookup_list(),
'selected_infrastructure_ids': [i['id'] for i in data['infrastructures']]}

tk.c.form = tk.render('metadata_record/edit_form.html', extra_vars=vars)
return tk.render('metadata_record/edit.html')

def delete(self, id, organization_id=None, metadata_collection_id=None):
if 'cancel' in tk.request.params:
tk.h.redirect_to('metadata_record_edit', id=id, organization_id=organization_id, metadata_collection_id=metadata_collection_id)

context = {'model': model, 'session': model.Session, 'user': tk.c.user}
try:
tk.check_access('metadata_record_delete', context, {'id': id})
except tk.NotAuthorized:
tk.abort(403, tk._('Unauthorized to delete metadata record'))

try:
if tk.request.method == 'POST':
tk.get_action('metadata_record_delete')(context, {'id': id})
tk.h.flash_notice(tk._('Metadata Record has been deleted.'))
tk.h.redirect_to('metadata_record_index', organization_id=organization_id, metadata_collection_id=metadata_collection_id)
tk.c.metadata_record = tk.get_action('metadata_record_show')(context, {'id': id})
except tk.NotAuthorized:
tk.abort(403, tk._('Unauthorized to delete metadata record'))
except tk.ObjectNotFound:
tk.abort(404, tk._('Metadata_record not found'))
return tk.render('metadata_record/confirm_delete.html')

def read(self, id, organization_id=None, metadata_collection_id=None):
self._set_containers_on_context(organization_id, metadata_collection_id)
context = {'model': model, 'session': model.Session, 'user': tk.c.user, 'for_view': True}
tk.c.metadata_record = tk.get_action('metadata_record_show')(context, {'id': id})
return tk.render('metadata_record/read.html')

def activity(self, id, organization_id=None, metadata_collection_id=None):
self._set_containers_on_context(organization_id, metadata_collection_id)
context = {'model': model, 'session': model.Session, 'user': tk.c.user, 'for_view': True}
tk.c.metadata_record = tk.get_action('metadata_record_show')(context, {'id': id})
return tk.render('metadata_record/activity_stream.html')

@staticmethod
def _metadata_standard_lookup_list():
"""
Return a list of {'value': name, 'text': display_name} dicts for populating the
metadata standard select control.
"""
context = {'model': model, 'session': model.Session, 'user': tk.c.user}
metadata_standards = tk.get_action('metadata_standard_list')(context, {'all_fields': True})
return [{'value': '', 'text': tk._('(None)')}] + \
[{'value': metadata_standard['name'], 'text': metadata_standard['display_name']}
for metadata_standard in metadata_standards]

@staticmethod
def _infrastructure_lookup_list():
"""
Return a list of {'value': name, 'text': display_name} dicts for populating the
infrastructure select control.
"""
context = {'model': model, 'session': model.Session, 'user': tk.c.user}
infrastructures = tk.get_action('infrastructure_list')(context, {'all_fields': True})
return [{'value': infrastructure['name'], 'text': infrastructure['display_name']}
for infrastructure in infrastructures]

def _save_new(self, context):
try:
data_dict = clean_dict(dict_fns.unflatten(tuplize_dict(parse_params(tk.request.params))))
data_dict['infrastructures'] = self._parse_infrastructure_ids(data_dict.get('infrastructure_ids'))
context['message'] = data_dict.get('log_message', '')
metadata_record = tk.get_action('metadata_record_create')(context, data_dict)
tk.h.redirect_to('metadata_record_read', id=metadata_record['id'],
organization_id=tk.c.organization['name'],
metadata_collection_id=tk.c.metadata_collection['name'])
except (tk.ObjectNotFound, tk.NotAuthorized):
tk.abort(404, tk._('Metadata record not found'))
except dict_fns.DataError:
tk.abort(400, tk._(u'Integrity Error'))
except tk.ValidationError, e:
errors = e.error_dict
error_summary = e.error_summary
return self.new(data_dict, errors, error_summary)

def _save_edit(self, id, context):
try:
data_dict = clean_dict(dict_fns.unflatten(tuplize_dict(parse_params(tk.request.params))))
data_dict['id'] = id
data_dict['infrastructures'] = self._parse_infrastructure_ids(data_dict.get('infrastructure_ids'))
context['message'] = data_dict.get('log_message', '')
context['allow_partial_update'] = True
metadata_record = tk.get_action('metadata_record_update')(context, data_dict)
tk.h.redirect_to('metadata_record_read', id=metadata_record['id'],
organization_id=tk.c.organization['name'],
metadata_collection_id=tk.c.metadata_collection['name'])
except (tk.ObjectNotFound, tk.NotAuthorized), e:
tk.abort(404, tk._('Metadata record not found'))
except dict_fns.DataError:
tk.abort(400, tk._(u'Integrity Error'))
except tk.ValidationError, e:
errors = e.error_dict
error_summary = e.error_summary
return self.edit(id, data_dict, errors, error_summary)

@staticmethod
def _parse_infrastructure_ids(infrastructure_ids):
if not infrastructure_ids:
return []
if isinstance(infrastructure_ids, basestring):
return [{'id': infrastructure_ids}]
return [{'id': infrastructure_id} for infrastructure_id in infrastructure_ids]
3 changes: 2 additions & 1 deletion ckanext/metadata/lib/dictization/model_dictize.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@ def metadata_record_dictize(pkg, context):
result = execute(q, package_rev, context).first()
if not result:
raise tk.ObjectNotFound

result_dict = d.table_dictize(result, context)
# strip whitespace from title
if result_dict.get('title'):
result_dict['title'] = result_dict['title'].strip()
result_dict['display_name'] = result_dict['title'] or result_dict['name'] or result_dict['id']

# extras
if is_latest_revision:
Expand Down
1 change: 1 addition & 0 deletions ckanext/metadata/logic/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ def metadata_record_show_schema(deserialize_json=False):
'workflow_state_id': [convert_from_extras, default(None), v.convert_id_to_name('workflow_state')],
'private': [],
'extras': _extras_schema(),
'display_name': [],
})
return schema

Expand Down
8 changes: 8 additions & 0 deletions ckanext/metadata/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ def before_map(self, map):
controller = 'ckanext.metadata.controllers.organization:OrganizationController'
map.connect('organization_datasets', '/organization/datasets/{id}', controller=controller, action='datasets', ckan_icon='site-map')

controller = 'ckanext.metadata.controllers.metadata_record:MetadataRecordController'
map.connect('metadata_record_index', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record', controller=controller, action='index')
map.connect('metadata_record_new', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record/new', controller=controller, action='new')
map.connect('metadata_record_edit', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record/edit/{id}', controller=controller, action='edit', ckan_icon='pencil-square-o')
map.connect('metadata_record_delete', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record/delete/{id}', controller=controller, action='delete')
map.connect('metadata_record_read', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record/{id}', controller=controller, action='read', ckan_icon='file-text-o')
map.connect('metadata_record_activity', '/organization/{organization_id}/metadata_collection/{metadata_collection_id}/metadata_record/activity/{id}', controller=controller, action='activity', ckan_icon='clock-o')

controller = 'ckanext.metadata.controllers.metadata_standard:MetadataStandardController'
map.connect('metadata_standard_index', '/metadata_standard', controller=controller, action='index')
map.connect('metadata_standard_new', '/metadata_standard/new', controller=controller, action='new')
Expand Down
1 change: 1 addition & 0 deletions ckanext/metadata/public/images/credits.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
metadata_schema: https://json-schema.org/assets/logo.svg
metadata_standard: Icon made by Smashicons from www.flaticon.com (search term: standard)
metadata_record: adapted from https://json-schema.org/assets/logo.svg
workflow_state: Icon made by Freepik from www.flaticon.com (search term: to do list)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions ckanext/metadata/templates/metadata_record/activity_stream.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "metadata_record/read_base.html" %}

{% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %}

{% block primary_content_inner %}
<h2 class="hide-heading">{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}</h2>
{% block activity_stream %}
{{ c.metadata_record_activity_stream | safe }}
{% endblock %}
{% endblock %}
7 changes: 7 additions & 0 deletions ckanext/metadata/templates/metadata_record/edit.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends "metadata_record/edit_base.html" %}

{% block page_heading_class %}hide-heading{% endblock %}
{% block page_heading %}{{ _('Edit Metadata Record') }}{% endblock %}
{% block subtitle %}
{{ _('Manage') }} - {{ c.metadata_record.display_name }} - {{ _('Metadata Records') }}
{% endblock %}
29 changes: 29 additions & 0 deletions ckanext/metadata/templates/metadata_record/edit_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{% extends "page.html" %}

{% block breadcrumb_content %}
{% snippet "metadata_record/snippets/breadcrumb_content_outer.html" %}
{% snippet "metadata_record/snippets/breadcrumb_content_item.html" %}
{% snippet "metadata_record/snippets/breadcrumb_content_manage.html" %}
{% endblock %}

{% block page_primary_action %}{% endblock %}

{% block primary_content_inner %}
<h1 class="{% block page_heading_class %}page-heading{% endblock %}">{% block page_heading %}{{ _('Metadata Record Form') }}{% endblock %}</h1>
{% block form %}
{{ c.form | safe }}
{% endblock %}
{% endblock %}

{% block content_action %}
{% link_for _('View'), controller='ckanext.metadata.controllers.metadata_record:MetadataRecordController',
action='read', id=c.metadata_record.id, organization_id=c.organization.name, metadata_collection_id=c.metadata_collection.name, class_='btn', icon='eye' %}
{% endblock %}

{% block content_primary_nav %}
{{ h.build_nav_icon('metadata_record_edit', _('Edit'), id=c.metadata_record.id, organization_id=c.organization.name, metadata_collection_id=c.metadata_collection.name) }}
{% endblock %}

{% block secondary_content %}
{% snippet "metadata_record/snippets/info.html", metadata_record=c.metadata_record %}
{% endblock %}
42 changes: 42 additions & 0 deletions ckanext/metadata/templates/metadata_record/edit_form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{% import "macros/form.html" as form %}

<form id="metadata-record-form" class="form-horizontal" action="" method="post">
{{ form.errors(error_summary) }}

{{ form.hidden('owner_org', c.organization.name) }}
{{ form.hidden('metadata_collection_id', c.metadata_collection.name) }}

{{ form.select('metadata_standard_id', label=_('Metadata Standard'), id='field-metadata-standard',
options=metadata_standard_lookup_list, selected=data.metadata_standard_id, error=errors.metadata_standard_id,
classes=['control-medium'], is_required=true) }}

{{ form.textarea('metadata_json', label=_('Metadata JSON'), id='field-metadata-json',
placeholder=_('The JSON metadata dictionary.'),
value=data.metadata_json, error=errors.metadata_json, is_required=true,
attrs={'style': 'font-family: monospace'}, rows=12) }}

{% call form.multiselect('infrastructure_ids', label=_('Infrastructures'), id='field-infrastructures',
options=infrastructure_lookup_list, selected=selected_infrastructure_ids, error=errors.infrastructures,
classes=['control-medium']) %}
{{ form.info(_('Select the infrastructure(s) with which to associate the metadata record. Use Ctrl+Click to select / deselect individual items.')) }}
{% endcall %}

{{ form.required_message() }}

<div class="form-actions">
{% if action == "edit" %}
{% if h.check_access('metadata_record_delete', {'id': data.id}) %}
<a class="btn btn-danger pull-left" data-module="confirm-action" data-module-content="{{ _('Are you sure you want to delete this Metadata Record?') }}"
href="{% url_for controller='ckanext.metadata.controllers.metadata_record:MetadataRecordController', action='delete', id=data.id, organization_id=data.owner_org, metadata_collection_id=data.metadata_collection_id %}">
{% block delete_button_text %}{{ _('Delete') }}{% endblock %}</a>
{% endif %}
{% endif %}
<button class="btn btn-primary" name="save" type="submit">
{%- if action == "edit" -%}
{{ _('Update Metadata Record') }}
{%- else -%}
{{ _('Create Metadata Record') }}
{%- endif -%}
</button>
</div>
</form>
Loading

0 comments on commit 1654b56

Please sign in to comment.