# pylint: disable=no-member
'''
Kata's action overrides.
'''
import datetime
import logging
import re
from pylons import c
from pylons.i18n import _
import ckan.logic.action.get
import ckan.logic.action.create
import ckan.logic.action.update
import ckan.logic.action.delete
from ckan.model import Related, Session, Package, repo
import ckan.model as model
from ckan.lib.search import index_for, rebuild
from ckan.lib.navl.validators import ignore_missing, ignore, not_empty
from ckan.logic.validators import url_validator
from ckan.logic import check_access, NotAuthorized, side_effect_free, ValidationError
from ckanext.kata import utils
from ckan.logic import get_action
_get_or_bust = ckan.logic.get_or_bust
_authz = model.authz
log = logging.getLogger(__name__) # pylint: disable=invalid-name
TITLE_MATCH = re.compile(r'^(title_)?\d?$')
@side_effect_free
[docs]def package_show(context, data_dict):
'''Return the metadata of a dataset (package) and its resources.
:param id: the id or name of the dataset
:type id: string
:rtype: dictionary
'''
# Called before showing the dataset in some interface (browser, API),
# or when adding package to Solr index (no validation / conversions then).
pkg_dict1 = ckan.logic.action.get.package_show(context, data_dict)
pkg_dict1 = utils.resource_to_dataset(pkg_dict1)
# Remove empty agents that come from padding the agent list in converters
if 'agent' in pkg_dict1:
agents = filter(None, pkg_dict1.get('agent', []))
pkg_dict1['agent'] = agents or []
# Normally logic function should not catch the raised errors
# but here it is needed so action package_show won't catch it instead
# Hiding information from API calls
try:
check_access('package_update', context)
except NotAuthorized:
pkg_dict1 = utils.hide_sensitive_fields(pkg_dict1)
pkg = Package.get(pkg_dict1['id'])
if 'erelated' in pkg.extras:
erelated = pkg.extras['erelated']
if len(erelated):
for value in erelated.split(';'):
if len(Session.query(Related).filter(Related.title == value).all()) == 0:
data_dict = {'title': value,
'type': _("Paper"),
'dataset_id': pkg.id}
related_create(context, data_dict)
# Update package.title to match package.extras.title_0
extras_title = pkg.extras.get(u'title_0')
if extras_title and extras_title != pkg.title:
repo.new_revision()
pkg.title = pkg.extras[u'title_0']
pkg.save()
rebuild(pkg.id) # Rebuild solr-index for this dataset
return pkg_dict1
[docs]def package_create(context, data_dict):
"""
Creates a new dataset.
Extends ckan's similar method to instantly reindex the SOLR index,
so that this newly added package emerges in search results instantly instead of
during the next timed reindexing.
:param context: context
:param data_dict: data dictionary (package data)
:rtype: dictionary
"""
user = model.User.get(context['user'])
try:
if data_dict['type'] == 'harvest' and not user.sysadmin:
ckan.lib.base.abort(401, _('Unauthorized to add a harvest source'))
except KeyError:
log.debug("Tried to check the package type, but it wasn't present!")
# TODO: JUHO: Dubious to let pass without checking user.sysadmin
pass
# Remove ONKI generated parameters for tidiness
# They won't exist when adding via API
try:
removable = ['field-tags', 'tag_string_tmp', 'field-tags_langs',
'geographic_coverage_field_langs', 'geographic_coverage_field',
'geographic_coverage_tmp',
'discipline_field_langs', 'discipline_field']
for key in removable:
del data_dict[key]
except KeyError:
pass
data_dict = utils.dataset_to_resource(data_dict)
# Get version PID (or generate a new one?)
new_version_pid = data_dict.get('new_version_pid')
if not new_version_pid and data_dict.get('generate_version_pid', None) == 'on':
new_version_pid = utils.generate_pid()
if new_version_pid:
data_dict['pids'] = data_dict.get('pids', []) + [{'id': new_version_pid, 'type': 'version', 'provider': 'kata'}]
# Add current user as a distributor if not already present.
if user:
if not 'agent' in data_dict:
data_dict['agent'] = []
user_name = user.display_name
distributor_names = [agent.get('name') for agent in data_dict['agent'] if agent.get('role') == 'distributor']
if not user_name in distributor_names:
data_dict['agent'].append(
{'name': user_name, 'role': 'distributor', 'id': user.id}
)
pkg_dict1 = ckan.logic.action.create.package_create(context, data_dict)
# Logging for production use
try:
log_str = '[' + str(datetime.datetime.now())
log_str += ']' + ' Package created ' + 'by: ' + context['user']
log_str += ' target: ' + pkg_dict1['id']
log.info(log_str)
except:
pass
context = {'model': model, 'ignore_auth': True, 'validate': False,
'extras_as_string': False}
pkg_dict = ckan.logic.action.get.package_show(context, pkg_dict1)
index = index_for('package')
index.index_package(pkg_dict)
return pkg_dict1
[docs]def package_update(context, data_dict):
'''
Updates the dataset.
Extends ckan's similar method to instantly re-index the SOLR index.
Otherwise the changes would only be added during a re-index (a rebuild of search index,
to be specific).
:type context: dict
:param context: context
:type data_dict: dict
:param data_dict: dataset as dictionary
:rtype: dictionary
'''
# Remove ONKI generated parameters for tidiness
# They won't exist when adding via API
try:
removable = ['field-tags', 'tag_string_tmp', 'field-tags_langs',
'geographic_coverage_field_langs', 'geographic_coverage_field',
'discipline_field_langs', 'discipline_field']
for key in removable:
del data_dict[key]
except KeyError:
pass
# Get all resources here since we get only 'dataset' resources from WUI.
temp_context = {'model': model, 'ignore_auth': True, 'validate': True,
'extras_as_string': True}
temp_pkg_dict = ckan.logic.action.get.package_show(temp_context, data_dict)
old_resources = temp_pkg_dict.get('resources', [])
if not 'resources' in data_dict:
# When this is reached, we are updating a dataset, not creating a new resource
data_dict['resources'] = old_resources
data_dict = utils.dataset_to_resource(data_dict)
# Get all PIDs (except for package.id and package.name) from database and add new relevant PIDS there
data_dict['pids'] = temp_pkg_dict.get('pids', [])
new_version_pid = data_dict.get('new_version_pid', None)
if not new_version_pid and data_dict.get('generate_version_pid', None) == 'on':
new_version_pid = utils.generate_pid()
if new_version_pid:
data_dict['pids'] += [{'id': new_version_pid,
'type': 'version',
'provider': 'kata',
}]
# # Check if data version has changed and if so, generate a new version_PID
# if not data_dict['version'] == temp_pkg_dict['version']:
# data_dict['pids'].append(
# {
# u'provider': u'kata',
# u'id': utils.generate_pid(),
# u'type': u'version',
# })
# This fixes extras fields being cleared when adding a resource. This is be because the extras are not properly
# cleared in show_package_schema conversions. Some fields stay in extras and they cause all other fields to be
# dropped in package_update(). When updating a dataset via UI or API, the conversion to extras occur in
# package_update() and popping extras here should have no effect.
data_dict.pop('extras', None)
# TODO: MIKKO: Get rid of popping extras here and rather pop the additional extras in converters so we could remove the
# popping and the above "context['allow_partial_update'] = True" which causes the extras to be processed in a way
# that nothing gets added to extras from the converters and everything not initially present in extras gets removed.
# TODO: JUHO: Apply correct schema depending on dataset
# This is quick resolution. More robust way would be to check through
# model.Package to which harvest source the dataset belongs and then get the
# type of the harvester (eg. DDI)
# if data_dict['name'].startswith('FSD'):
# context['schema'] = schemas.update_package_schema_ddi()
pkg_dict1 = ckan.logic.action.update.package_update(context, data_dict)
# Logging for production use
try:
log_str = '[' + str(datetime.datetime.now())
log_str += ']' + ' Package updated ' + 'by: ' + context['user']
log_str += ' target: ' + data_dict['id']
log.info(log_str)
except:
pass
context = {'model': model, 'ignore_auth': True, 'validate': False,
'extras_as_string': True}
pkg_dict = ckan.logic.action.get.package_show(context, pkg_dict1)
index = index_for('package')
# update_dict calls index_package, so it would basically be the same
index.update_dict(pkg_dict)
return pkg_dict1
[docs]def package_delete(context, data_dict):
'''
Deletes a package
Extends ckan's similar method to instantly re-index the SOLR index.
Otherwise the changes would only be added during a re-index (a rebuild of search index,
to be specific).
:param context: context
:type context: dictionary
:param data_dict: package data
:type data_dict: dictionary
'''
# Logging for production use
try:
log_str = '[' + str(datetime.datetime.now())
log_str += ']' + ' Package deleted ' + 'by: ' + context['user']
log_str += ' target: ' + data_dict['id']
log.info(log_str)
except:
pass
index = index_for('package')
index.remove_dict(data_dict)
ret = ckan.logic.action.delete.package_delete(context, data_dict)
return ret
# Log should show who did what and when
def _decorate(f, actiontype, action):
def call(*args, **kwargs):
log_str = '[ ' + actiontype + ' ] [ ' + str(datetime.datetime.now())
if action is 'delete':
# log id before we delete the data
try:
log_str += ' ] ' + actiontype + ' deleted by: ' + args[0]['user']
log_str += ' target: ' + args[1]['id']
log.info(log_str)
except:
log.info('Debug failed! Action not logged')
ret = f(*args, **kwargs)
if action is 'create' or action is 'update':
try:
log_str += ' ] ' + actiontype + ' ' + action + 'd by: ' + args[0]['user']
log_str += ' target: ' + ret['id']
log.info(log_str)
except:
log.info('Debug failed! Action not logged')
return ret
return call
# Overwriting to add logging
resource_create = _decorate(ckan.logic.action.create.resource_create, 'resource', 'create')
resource_update = _decorate(ckan.logic.action.update.resource_update, 'resource', 'update')
resource_delete = _decorate(ckan.logic.action.delete.resource_delete, 'resource', 'delete')
related_delete = _decorate(ckan.logic.action.delete.related_delete, 'related', 'delete')
member_create = _decorate(ckan.logic.action.create.member_create, 'member', 'create')
member_delete = _decorate(ckan.logic.action.delete.member_delete, 'member', 'delete')
group_create = _decorate(ckan.logic.action.create.group_create, 'group', 'create')
group_update = _decorate(ckan.logic.action.update.group_update, 'group', 'update')
# group_list = _decorate(ckan.logic.action.get.group_list, 'group', 'list')
group_delete = _decorate(ckan.logic.action.delete.group_delete, 'group', 'delete')
organization_create = _decorate(ckan.logic.action.create.organization_create, 'organization', 'create')
organization_update = _decorate(ckan.logic.action.update.organization_update, 'organization', 'update')
organization_delete = _decorate(ckan.logic.action.delete.organization_delete, 'organization', 'delete')
@side_effect_free
[docs]def package_search(context, data_dict):
'''Return the metadata of a dataset (package) and its resources.
:param id: the id or name of the dataset
:type id: string
:param context: context
:type context: dictionary
:rtype: dictionary
'''
#Wraps around the CKAN package_search action to add customizations
#in some special cases.
if c.controller == "home" and c.action == "index":
data_dict['sort'] = "metadata_modified desc"
data_dict['rows'] = 5
# don't want harvest source packages
data_dict['fq'] += " +dataset_type:dataset"
return ckan.logic.action.get.package_search(context, data_dict)
[docs]def group_list(context, data_dict):
'''
Return a list of the names of the site's groups.
'''
if not "for_view" in context:
return []
else:
return ckan.logic.action.get.group_list(context, data_dict)
[docs]def dataset_editor_delete(context, data_dict):
'''
Deletes user and role in a dataset
:param username: user name to delete
:type username: string
:param id: dataset id
:type id: string
:param role: editor, admin or reader
:type role: string
:rtype: message dict with `success` and `msg`
'''
pkg = model.Package.get(data_dict.get('name', None))
user = model.User.get(context.get('user', None))
role = data_dict.get('role', None)
username = model.User.get(data_dict.get('username', None))
if not (pkg and user and role):
msg = _('Required information missing')
return {'success': False, 'msg': msg}
pkg_dict = get_action('package_show')(context, {'id': pkg.id})
pkg_dict['domain_object'] = pkg_dict.get('id')
domain_object_ref = _get_or_bust(pkg_dict, 'domain_object')
# This could be simpler, as domain_object_ref is pkg.id
domain_object = ckan.logic.action.get_domain_object(model, domain_object_ref)
# Todo: use check_access instead? It is not this detailed, though
if not username:
msg = _('User not found')
return {'success': False, 'msg': msg}
if not (_authz.user_has_role(user, role, domain_object) or
_authz.user_has_role(user, 'admin', domain_object) or
role == 'reader' or user.sysadmin == True):
msg = _('No sufficient privileges to remove user from role %s.') % role
return {'success': False, 'msg': msg}
if not _authz.user_has_role(username, role, pkg):
msg = _('No such user and role combination')
return {'success': False, 'msg': msg}
if username.name == 'visitor' or username.name == 'logged_in':
msg = _('Built-in users can not be removed')
return {'success': False, 'msg': msg}
if user.id == username.id:
msg = _('You can not remove yourself')
return {'success': False, 'msg': msg}
_authz.remove_user_from_role(username, role, pkg)
msg = _('User removed from role %s') % role
return {'success': True, 'msg': msg}
[docs]def dataset_editor_add(context, data_dict):
'''
Adds a user and role to dataset
:param name: dataset name
:type name: string
:param role: admin, editor or reader
:type role: string
:param username: user to be added
:type username: string
:rtype: message dict with 'success' and 'msg'
'''
pkg = model.Package.get(data_dict.get('name', None))
user = model.User.get(context.get('user', None))
role = data_dict.get('role', None)
username = model.User.get(data_dict.get('username', None))
if not (pkg and user and role):
msg = _('Required information missing')
return {'success': False, 'msg': msg}
pkg_dict = get_action('package_show')(context, {'id': pkg.id})
pkg_dict['domain_object'] = pkg_dict.get('id')
domain_object_ref = _get_or_bust(pkg_dict, 'domain_object')
domain_object = ckan.logic.action.get_domain_object(model, domain_object_ref)
# Todo: use check_access instead? It is not this detailed, though
if not username:
msg = _('User not found')
return {'success': False, 'msg': msg}
if not (_authz.user_has_role(user, role, domain_object) or
_authz.user_has_role(user, 'admin', domain_object) or
role == 'reader' or user.sysadmin == True):
msg = _('No sufficient privileges to add a user to role %s.') % role
return {'success': False, 'msg': msg}
if _authz.user_has_role(username, role, domain_object):
msg = _('User already has %s rights') % role
return {'success': False, 'msg': msg}
if user.id == username.id:
msg = _('You can not add yourself')
return {'success': False, 'msg': msg}
model.add_user_to_role(username, role, pkg)
model.meta.Session.commit()
msg = _('User added')
return {'success': True, 'msg': msg}