diff --git a/ckanext/metadata/controllers/metadata_record.py b/ckanext/metadata/controllers/metadata_record.py new file mode 100644 index 0000000..72a16a5 --- /dev/null +++ b/ckanext/metadata/controllers/metadata_record.py @@ -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] diff --git a/ckanext/metadata/lib/dictization/model_dictize.py b/ckanext/metadata/lib/dictization/model_dictize.py index c3d41ca..2e6c12b 100644 --- a/ckanext/metadata/lib/dictization/model_dictize.py +++ b/ckanext/metadata/lib/dictization/model_dictize.py @@ -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: diff --git a/ckanext/metadata/logic/schema.py b/ckanext/metadata/logic/schema.py index 8a93c73..c4964b1 100644 --- a/ckanext/metadata/logic/schema.py +++ b/ckanext/metadata/logic/schema.py @@ -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 diff --git a/ckanext/metadata/plugin.py b/ckanext/metadata/plugin.py index 1ed6591..e54dacd 100644 --- a/ckanext/metadata/plugin.py +++ b/ckanext/metadata/plugin.py @@ -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') diff --git a/ckanext/metadata/public/images/credits.txt b/ckanext/metadata/public/images/credits.txt index 708a027..8857bf4 100644 --- a/ckanext/metadata/public/images/credits.txt +++ b/ckanext/metadata/public/images/credits.txt @@ -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) diff --git a/ckanext/metadata/public/images/metadata_record.png b/ckanext/metadata/public/images/metadata_record.png new file mode 100644 index 0000000..02c9798 Binary files /dev/null and b/ckanext/metadata/public/images/metadata_record.png differ diff --git a/ckanext/metadata/templates/metadata_record/activity_stream.html b/ckanext/metadata/templates/metadata_record/activity_stream.html new file mode 100644 index 0000000..45097a6 --- /dev/null +++ b/ckanext/metadata/templates/metadata_record/activity_stream.html @@ -0,0 +1,10 @@ +{% extends "metadata_record/read_base.html" %} + +{% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} + +{% block primary_content_inner %} +
+ {{ _('There are currently no metadata records for this site') }}. + {% if h.check_access('metadata_record_create') %} + {% link_for _('How about creating one?'), action='new', organization_id=c.organization.name, metadata_collection_id=c.metadata_collection.name, + controller='ckanext.metadata.controllers.metadata_record:MetadataRecordController' %}. + {% endif %} +
+ {% endif %} + {% endblock %} + {% block page_pagination %} + {{ c.page.pager(q=c.q or '', sort=c.sort_by_selected or '') }} + {% endblock %} +{% endblock %} + +{% block secondary_content %} + {% snippet "metadata_record/snippets/helper.html" %} +{% endblock %} diff --git a/ckanext/metadata/templates/metadata_record/new.html b/ckanext/metadata/templates/metadata_record/new.html new file mode 100644 index 0000000..384ed08 --- /dev/null +++ b/ckanext/metadata/templates/metadata_record/new.html @@ -0,0 +1,19 @@ +{% extends "metadata_record/edit_base.html" %} + +{% block page_heading %}{{ _('Create a Metadata Record') }}{% endblock %} +{% block page_header %}{% endblock %} +{% block subtitle %}{{ _('Create a Metadata Record') }}{% endblock %} + +{% block breadcrumb_content %} + {% snippet "metadata_record/snippets/breadcrumb_content_outer.html" %} ++ {% trans %} + A metadata record is a specialized type of CKAN dataset encapsulating a + JSON metadata dictionary that describes a digital object. The metadata + dictionary may be validated against one or more metadata schemas. + {% endtrans %} +
++ {{ h.markdown_extract(h.get_translated(metadata_record, 'description'), 180) }} +
+ {% endif %} + {% endblock %} + {% endblock %} +{{ h.markdown_extract(h.get_translated(metadata_record, 'description'), extract_length=80) }}
+ {% endif %} + {% endblock %} + {% block link %} + + {{ _('View {name}').format(name=metadata_record.display_name) }} + + {% endblock %} + {% endblock %} +