diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py
index a1480a5fda0..e711fea8561 100644
--- a/ckan/controllers/group.py
+++ b/ckan/controllers/group.py
@@ -270,8 +270,9 @@ def drill_down_url(**by):
c.drill_down_url = drill_down_url
def remove_field(key, value=None, replace=None):
+ controller = lookup_group_controller(group_type)
return h.remove_url_param(key, value=value, replace=replace,
- controller='group', action='read',
+ controller=controller, action='read',
extras=dict(id=c.group_dict.get('name')))
c.remove_field = remove_field
@@ -283,6 +284,7 @@ def pager_url(q=None, page=None):
try:
c.fields = []
+ c.fields_grouped = {}
search_extras = {}
for (param, value) in request.params.items():
if param not in ['q', 'page', 'sort'] \
@@ -290,6 +292,10 @@ def pager_url(q=None, page=None):
if not param.startswith('ext_'):
c.fields.append((param, value))
q += ' %s: "%s"' % (param, value)
+ if param not in c.fields_grouped:
+ c.fields_grouped[param] = [value]
+ else:
+ c.fields_grouped[param].append(value)
else:
search_extras[param] = value
diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py
index aa7d7efeafd..777bda5fa5e 100644
--- a/ckan/controllers/package.py
+++ b/ckan/controllers/package.py
@@ -579,12 +579,7 @@ def resource_edit(self, id, resource_id, data=None, errors=None,
# dataset has not yet been fully created
resource_dict = get_action('resource_show')(context,
{'id': resource_id})
- fields = ['url', 'resource_type', 'format', 'name', 'description',
- 'id']
- data = {}
- for field in fields:
- data[field] = resource_dict[field]
- return self.new_resource(id, data=data)
+ return self.new_resource(id, data=resource_dict)
# resource is fully created
try:
resource_dict = get_action('resource_show')(context,
@@ -1041,7 +1036,12 @@ def resource_delete(self, id, resource_id):
if request.method == 'POST':
get_action('resource_delete')(context, {'id': resource_id})
h.flash_notice(_('Resource has been deleted.'))
- h.redirect_to(controller='package', action='read', id=id)
+ pkg_dict = get_action('package_show')(None, {'id': id})
+ if pkg_dict['state'].startswith('draft'):
+ h.redirect_to(controller='package', action='new_resource',
+ id=id)
+ else:
+ h.redirect_to(controller='package', action='read', id=id)
c.resource_dict = get_action('resource_show')(
context, {'id': resource_id})
c.pkg_id = id
diff --git a/ckan/lib/mailer.py b/ckan/lib/mailer.py
index 9eaf2fd6813..c75e3435e96 100644
--- a/ckan/lib/mailer.py
+++ b/ckan/lib/mailer.py
@@ -144,11 +144,11 @@ def get_invite_body(user, group_dict=None, role=None):
def get_reset_link(user):
- return urljoin(config.get('site_url'),
- h.url_for(controller='user',
- action='perform_reset',
- id=user.id,
- key=user.reset_key))
+ return h.url_for(controller='user',
+ action='perform_reset',
+ id=user.id,
+ key=user.reset_key,
+ qualified=True)
def send_reset_link(user):
diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py
index 9d96565b764..d7b92a29e9a 100644
--- a/ckan/logic/action/get.py
+++ b/ckan/logic/action/get.py
@@ -336,7 +336,7 @@ def _group_or_org_list(context, data_dict, is_org=False):
data_dict, logic.schema.default_pagination_schema(), context)
if errors:
raise ValidationError(errors)
- sort = data_dict.get('sort') or 'name'
+ sort = data_dict.get('sort') or 'title'
q = data_dict.get('q')
all_fields = asbool(data_dict.get('all_fields', None))
@@ -1831,8 +1831,9 @@ def package_search(context, data_dict):
fl
The parameter that controls which fields are returned in the solr
- query cannot be changed. CKAN always returns the matched datasets as
- dictionary objects.
+ query.
+ fl can be None or a list of result fields, such as ['id', 'extras_custom_field'].
+ if fl = None, datasets are returned as a list of full dictionary.
'''
# sometimes context['schema'] is None
schema = (context.get('schema') or
@@ -1875,8 +1876,12 @@ def package_search(context, data_dict):
else:
data_source = 'validated_data_dict'
data_dict.pop('use_default_schema', None)
- # return a list of package ids
- data_dict['fl'] = 'id {0}'.format(data_source)
+
+ result_fl = data_dict.get('fl')
+ if not result_fl:
+ data_dict['fl'] = 'id {0}'.format(data_source)
+ else:
+ data_dict['fl'] = ' '.join(result_fl)
# Remove before these hit solr FIXME: whitelist instead
include_private = asbool(data_dict.pop('include_private', False))
@@ -1903,21 +1908,28 @@ def package_search(context, data_dict):
# Add them back so extensions can use them on after_search
data_dict['extras'] = extras
- for package in query.results:
- # get the package object
- package_dict = package.get(data_source)
- ## use data in search index if there
- if package_dict:
- # the package_dict still needs translating when being viewed
- package_dict = json.loads(package_dict)
- if context.get('for_view'):
- for item in plugins.PluginImplementations(
- plugins.IPackageController):
- package_dict = item.before_view(package_dict)
- results.append(package_dict)
- else:
- log.error('No package_dict is coming from solr for package '
- 'id %s', package['id'])
+ if result_fl:
+ for package in query.results:
+ if package.get('extras'):
+ package.update(package['extras'] )
+ package.pop('extras')
+ results.append(package)
+ else:
+ for package in query.results:
+ # get the package object
+ package_dict = package.get(data_source)
+ ## use data in search index if there
+ if package_dict:
+ # the package_dict still needs translating when being viewed
+ package_dict = json.loads(package_dict)
+ if context.get('for_view'):
+ for item in plugins.PluginImplementations(
+ plugins.IPackageController):
+ package_dict = item.before_view(package_dict)
+ results.append(package_dict)
+ else:
+ log.error('No package_dict is coming from solr for package '
+ 'id %s', package['id'])
count = query.count
facets = query.facets
diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py
index 286c536e4df..4a6eff941d3 100644
--- a/ckan/logic/schema.py
+++ b/ckan/logic/schema.py
@@ -587,6 +587,7 @@ def default_autocomplete_schema():
def default_package_search_schema():
schema = {
'q': [ignore_missing, unicode],
+ 'fl': [ignore_missing, list_of_strings],
'fq': [ignore_missing, unicode],
'rows': [ignore_missing, natural_number_validator],
'sort': [ignore_missing, unicode],
diff --git a/ckan/model/activity.py b/ckan/model/activity.py
index fdfa52660cc..2c3717dc902 100644
--- a/ckan/model/activity.py
+++ b/ckan/model/activity.py
@@ -3,7 +3,16 @@
import datetime
from sqlalchemy import (
- orm, types, Column, Table, ForeignKey, desc, or_, union_all)
+ orm,
+ types,
+ Column,
+ Table,
+ ForeignKey,
+ desc,
+ or_,
+ and_,
+ union_all
+)
import ckan.model
import meta
@@ -182,14 +191,33 @@ def _group_activity_query(group_id):
# Return a query with no results.
return model.Session.query(model.Activity).filter("0=1")
- dataset_ids = [dataset.id for dataset in group.packages()]
+ q = model.Session.query(
+ model.Activity
+ ).outerjoin(
+ model.Member,
+ and_(
+ model.Activity.object_id == model.Member.table_id,
+ model.Member.state == 'active'
+ )
+ ).outerjoin(
+ model.Package,
+ and_(
+ model.Package.id == model.Member.table_id,
+ model.Package.private == False,
+ model.Package.state == 'active'
+ )
+ ).filter(
+ # We only care about activity either on the the group itself or on
+ # packages within that group.
+ # FIXME: This means that activity that occured while a package belonged
+ # to a group but was then removed will not show up. This may not be
+ # desired but is consistent with legacy behaviour.
+ or_(
+ model.Member.group_id == group_id,
+ model.Activity.object_id == group_id
+ ),
+ )
- q = model.Session.query(model.Activity)
- if dataset_ids:
- q = q.filter(or_(model.Activity.object_id == group_id,
- model.Activity.object_id.in_(dataset_ids)))
- else:
- q = q.filter(model.Activity.object_id == group_id)
return q
diff --git a/ckan/templates/organization/index.html b/ckan/templates/organization/index.html
index d4079f318cf..4214ce6b165 100644
--- a/ckan/templates/organization/index.html
+++ b/ckan/templates/organization/index.html
@@ -17,7 +17,7 @@
{% block primary_content_inner %}
{% block page_heading %}{{ _('Organizations') }}{% endblock %}
{% block organizations_search_form %}
- {% snippet 'snippets/search_form.html', form_id='organization-search-form', type='organization', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search organizations...'), show_empty=request.params, no_bottom_border=true if c.page.items %}
+ {% snippet 'snippets/search_form.html', form_id='organization-search-form', type='organization', query=c.q, sorting_selected=c.sort_by_selected, count=c.page.item_count, placeholder=_('Search organizations...'), show_empty=request.params, no_bottom_border=true if c.page.items, sorting = [(_('Name Ascending'), 'title asc'), (_('Name Descending'), 'title desc')] %}
{% endblock %}
{% block organizations_list %}
{% if c.page.items or request.params %}
diff --git a/ckan/templates/package/resource_edit_base.html b/ckan/templates/package/resource_edit_base.html
index 362e1b59caa..a3e952c9506 100644
--- a/ckan/templates/package/resource_edit_base.html
+++ b/ckan/templates/package/resource_edit_base.html
@@ -22,9 +22,7 @@
{% block content_primary_nav %}
{{ h.build_nav_icon('resource_edit', _('Edit resource'), id=pkg.name, resource_id=res.id) }}
- {% if 'datapusher' in g.plugins %}
- {{ h.build_nav_icon('resource_data', _('DataStore'), id=pkg.name, resource_id=res.id) }}
- {% endif %}
+ {% block inner_primary_nav %}{% endblock %}
{{ h.build_nav_icon('views', _('Views'), id=pkg.name, resource_id=res.id) }}
{% endblock %}
diff --git a/ckan/tests/logic/action/test_get.py b/ckan/tests/logic/action/test_get.py
index 8800c5b1980..c3f49f3b0db 100644
--- a/ckan/tests/logic/action/test_get.py
+++ b/ckan/tests/logic/action/test_get.py
@@ -852,6 +852,14 @@ def test_search(self):
eq(search_result['results'][0]['title'], 'Rivers')
eq(search_result['count'], 1)
+ def test_search_fl(self):
+ factories.Dataset(title='Rivers', name='test_ri')
+ factories.Dataset(title='Lakes')
+
+ search_result = helpers.call_action('package_search', q='rivers', fl=['title', 'name'])
+
+ eq(search_result['results'], [{'title': 'Rivers', 'name': 'test_ri'}])
+
def test_search_all(self):
factories.Dataset(title='Rivers')
factories.Dataset(title='Lakes')
diff --git a/ckanext/datapusher/plugin.py b/ckanext/datapusher/plugin.py
index a7db0f66aa7..762bb7317ea 100644
--- a/ckanext/datapusher/plugin.py
+++ b/ckanext/datapusher/plugin.py
@@ -67,11 +67,12 @@ def resource_data(self, id, resource_id):
except logic.NotAuthorized:
base.abort(403, _('Not authorized to see this page'))
- return base.render('package/resource_data.html',
+ return base.render('datapusher/resource_data.html',
extra_vars={'status': datapusher_status})
class DatapusherPlugin(p.SingletonPlugin):
+ p.implements(p.IConfigurer, inherit=True)
p.implements(p.IConfigurable, inherit=True)
p.implements(p.IActions)
p.implements(p.IAuthFunctions)
@@ -83,6 +84,9 @@ class DatapusherPlugin(p.SingletonPlugin):
legacy_mode = False
resource_show_action = None
+ def update_config(self, config):
+ p.toolkit.add_template_directory(config, 'templates')
+
def configure(self, config):
self.config = config
diff --git a/ckan/templates/package/resource_data.html b/ckanext/datapusher/templates/datapusher/resource_data.html
similarity index 100%
rename from ckan/templates/package/resource_data.html
rename to ckanext/datapusher/templates/datapusher/resource_data.html
diff --git a/ckanext/datapusher/templates/package/resource_edit_base.html b/ckanext/datapusher/templates/package/resource_edit_base.html
new file mode 100644
index 00000000000..736c434c0b8
--- /dev/null
+++ b/ckanext/datapusher/templates/package/resource_edit_base.html
@@ -0,0 +1,6 @@
+{% ckan_extends %}
+
+{% block inner_primary_nav %}
+ {{ super() }}
+ {{ h.build_nav_icon('resource_data', _('DataStore'), id=pkg.name, resource_id=res.id) }}
+{% endblock %}
diff --git a/ckanext/datastore/controller.py b/ckanext/datastore/controller.py
index a2bd391a6ea..2d11e3d5adf 100644
--- a/ckanext/datastore/controller.py
+++ b/ckanext/datastore/controller.py
@@ -1,12 +1,14 @@
# encoding: utf-8
import StringIO
+import md5
import pylons
from ckan.plugins.toolkit import (
Invalid,
ObjectNotFound,
+ NotAuthorized,
get_action,
get_validator,
_,
@@ -14,6 +16,9 @@
response,
BaseController,
abort,
+ render,
+ c,
+ h,
)
from ckanext.datastore.writer import (
csv_writer,
@@ -84,3 +89,42 @@ def result_page(offset, limit):
limit -= PAGINATE_BY
result = result_page(offset, limit)
+
+ def dictionary(self, id, resource_id):
+ u'''data dictionary view: show/edit field labels and descriptions'''
+
+ try:
+ # resource_edit_base template uses these
+ c.pkg_dict = get_action('package_show')(
+ None, {'id': id})
+ c.resource = get_action('resource_show')(
+ None, {'id': resource_id})
+ rec = get_action('datastore_search')(None, {
+ 'resource_id': resource_id,
+ 'limit': 0})
+ except (ObjectNotFound, NotAuthorized):
+ abort(404, _('Resource not found'))
+
+ fields = [f for f in rec['fields'] if not f['id'].startswith('_')]
+
+ if request.method == 'POST':
+ get_action('datastore_create')(None, {
+ 'resource_id': resource_id,
+ 'force': True,
+ 'fields': [{
+ 'id': f['id'],
+ 'type': f['type'],
+ 'info': {
+ 'label': request.POST.get('f{0}label'.format(i)),
+ 'notes': request.POST.get('f{0}notes'.format(i)),
+ }} for i, f in enumerate(fields, 1)]})
+
+ h.redirect_to(
+ controller='ckanext.datastore.controller:DatastoreController',
+ action='dictionary',
+ id=id,
+ resource_id=resource_id)
+
+ return render(
+ 'datastore/dictionary.html',
+ extra_vars={'fields': fields})
diff --git a/ckanext/datastore/db.py b/ckanext/datastore/db.py
index 1bb62871438..ed01ce95bfd 100644
--- a/ckanext/datastore/db.py
+++ b/ckanext/datastore/db.py
@@ -226,6 +226,24 @@ def _guess_type(field):
return 'text'
+def _get_field_info(connection, resource_id):
+ u'''return a dictionary mapping column names to their info data,
+ when present'''
+ qtext = sqlalchemy.text(u'''
+ select pa.attname as name, pd.description as info
+ from pg_class pc, pg_attribute pa, pg_description pd
+ where pa.attrelid = pc.oid and pd.objoid = pc.oid
+ and pd.objsubid = pa.attnum and pc.relname = :res_id
+ and pa.attnum > 0
+ ''')
+ try:
+ return dict(
+ (n, json.loads(v)) for (n, v) in
+ connection.execute(qtext, res_id=resource_id).fetchall())
+ except ValueError: # don't die on non-json comments
+ return {}
+
+
def _get_fields(context, data_dict):
fields = []
all_fields = context['connection'].execute(
@@ -339,15 +357,26 @@ def create_table(context, data_dict):
})
fields = datastore_fields + supplied_fields + extra_fields
- sql_fields = u", ".join([u'"{0}" {1}'.format(
- f['id'], f['type']) for f in fields])
+ sql_fields = u", ".join([u'{0} {1}'.format(
+ datastore_helpers.identifier(f['id']), f['type']) for f in fields])
- sql_string = u'CREATE TABLE "{0}" ({1});'.format(
- data_dict['resource_id'],
+ sql_string = u'CREATE TABLE {0} ({1});'.format(
+ datastore_helpers.identifier(data_dict['resource_id']),
sql_fields
)
- context['connection'].execute(sql_string.replace('%', '%%'))
+ info_sql = []
+ for f in supplied_fields:
+ info = f.get(u'info')
+ if isinstance(info, dict):
+ info_sql.append(u'COMMENT ON COLUMN {0}.{1} is {2}'.format(
+ datastore_helpers.identifier(data_dict['resource_id']),
+ datastore_helpers.identifier(f['id']),
+ datastore_helpers.literal_string(
+ json.dumps(info, ensure_ascii=False))))
+
+ context['connection'].execute(
+ (sql_string + u';'.join(info_sql)).replace(u'%', u'%%'))
def _get_aliases(context, data_dict):
@@ -607,12 +636,29 @@ def alter_table(context, data_dict):
'type': _guess_type(records[0][field_id])
})
- for field in new_fields:
- sql = 'ALTER TABLE "{0}" ADD "{1}" {2}'.format(
- data_dict['resource_id'],
- field['id'],
- field['type'])
- context['connection'].execute(sql.replace('%', '%%'))
+ alter_sql = []
+ for f in new_fields:
+ alter_sql.append(u'ALTER TABLE {0} ADD {1} {2};'.format(
+ datastore_helpers.identifier(data_dict['resource_id']),
+ datastore_helpers.identifier(f['id']),
+ f['type']))
+
+ for f in supplied_fields:
+ if u'info' in f:
+ info = f.get(u'info')
+ if isinstance(info, dict):
+ info_sql = datastore_helpers.literal_string(
+ json.dumps(info, ensure_ascii=False))
+ else:
+ info_sql = 'NULL'
+ alter_sql.append(u'COMMENT ON COLUMN {0}.{1} is {2}'.format(
+ datastore_helpers.identifier(data_dict['resource_id']),
+ datastore_helpers.identifier(f['id']),
+ info_sql))
+
+ if alter_sql:
+ context['connection'].execute(
+ u';'.join(alter_sql).replace(u'%', u'%%'))
def insert_data(context, data_dict):
@@ -994,7 +1040,8 @@ def search_data(context, data_dict):
results = _execute_single_statement(context, sql_string, where_values)
_insert_links(data_dict, limit, offset)
- r = format_results(context, results, data_dict)
+ r = format_results(context, results, data_dict, _get_field_info(
+ context['connection'], data_dict['resource_id']))
if data_dict.get('include_total', True):
count_sql_string = u'''SELECT {distinct} count(*)
@@ -1021,13 +1068,16 @@ def _execute_single_statement(context, sql_string, where_values):
return results
-def format_results(context, results, data_dict):
+def format_results(context, results, data_dict, field_info=None):
result_fields = []
for field in results.cursor.description:
- result_fields.append({
+ f = {
'id': field[0].decode('utf-8'),
'type': _get_type(context, field[1])
- })
+ }
+ if field_info and f['id'] in field_info:
+ f['info'] = field_info[f['id']]
+ result_fields.append(f)
records = []
for row in results:
diff --git a/ckanext/datastore/helpers.py b/ckanext/datastore/helpers.py
index 02a6f5aac84..406e1bcc5fc 100644
--- a/ckanext/datastore/helpers.py
+++ b/ckanext/datastore/helpers.py
@@ -6,6 +6,8 @@
import paste.deploy.converters as converters
import sqlparse
+from ckan.plugins.toolkit import get_action, ObjectNotFound, NotAuthorized
+
log = logging.getLogger(__name__)
@@ -107,3 +109,16 @@ def identifier(s):
Return s as a quoted postgres identifier
"""
return u'"' + s.replace(u'"', u'""').replace(u'\0', '') + u'"'
+
+
+def datastore_dictionary(resource_id):
+ """
+ Return the data dictionary info for a resource
+ """
+ try:
+ return [
+ f for f in get_action('datastore_search')(
+ None, {u'resource_id': resource_id, u'limit': 0})['fields']
+ if not f['id'].startswith(u'_')]
+ except (ObjectNotFound, NotAuthorized):
+ return []
diff --git a/ckanext/datastore/logic/action.py b/ckanext/datastore/logic/action.py
index e519670bf19..27e74599668 100644
--- a/ckanext/datastore/logic/action.py
+++ b/ckanext/datastore/logic/action.py
@@ -149,47 +149,7 @@ def datastore_create(context, data_dict):
log.debug(
'Setting datastore_active=True on resource {0}'.format(resource.id)
)
- # issue #3245: race condition
- update_dict = {'datastore_active': True}
-
- # get extras(for entity update) and package_id(for search index update)
- res_query = model.Session.query(
- model.resource_table.c.extras,
- model.resource_table.c.package_id
- ).filter(
- model.Resource.id == data_dict['resource_id']
- )
- extras, package_id = res_query.one()
-
- # update extras in database for record and its revision
- extras.update(update_dict)
- res_query.update({'extras': extras}, synchronize_session=False)
-
- model.Session.query(model.resource_revision_table).filter(
- model.ResourceRevision.id == data_dict['resource_id'],
- model.ResourceRevision.current is True
- ).update({'extras': extras}, synchronize_session=False)
-
- model.Session.commit()
-
- # get package with updated resource from solr
- # find changed resource, patch it and reindex package
- psi = search.PackageSearchIndex()
- solr_query = search.PackageSearchQuery()
- q = {
- 'q': 'id:"{0}"'.format(package_id),
- 'fl': 'data_dict',
- 'wt': 'json',
- 'fq': 'site_id:"%s"' % config.get('ckan.site_id'),
- 'rows': 1
- }
- for record in solr_query.run(q)['results']:
- solr_data_dict = json.loads(record['data_dict'])
- for resource in solr_data_dict['resources']:
- if resource['id'] == data_dict['resource_id']:
- resource.update(update_dict)
- psi.index_package(solr_data_dict)
- break
+ set_datastore_active_flag(model, data_dict, True)
result.pop('id', None)
result.pop('private', None)
@@ -396,11 +356,9 @@ def datastore_delete(context, data_dict):
if (not data_dict.get('filters') and
resource.extras.get('datastore_active') is True):
log.debug(
- 'Setting datastore_active=True on resource {0}'.format(resource.id)
+ 'Setting datastore_active=False on resource {0}'.format(resource.id)
)
- p.toolkit.get_action('resource_patch')(
- context, {'id': data_dict['resource_id'],
- 'datastore_active': False})
+ set_datastore_active_flag(model, data_dict, False)
result.pop('id', None)
result.pop('connection_url')
@@ -598,6 +556,56 @@ def datastore_make_public(context, data_dict):
db.make_public(context, data_dict)
+def set_datastore_active_flag(model, data_dict, flag):
+ '''
+ Set appropriate datastore_active flag on CKAN resource.
+
+ Called after creation or deletion of DataStore table.
+ '''
+ # We're modifying the resource extra directly here to avoid a
+ # race condition, see issue #3245 for details and plan for a
+ # better fix
+ update_dict = {'datastore_active': flag}
+
+ # get extras(for entity update) and package_id(for search index update)
+ res_query = model.Session.query(
+ model.resource_table.c.extras,
+ model.resource_table.c.package_id
+ ).filter(
+ model.Resource.id == data_dict['resource_id']
+ )
+ extras, package_id = res_query.one()
+
+ # update extras in database for record and its revision
+ extras.update(update_dict)
+ res_query.update({'extras': extras}, synchronize_session=False)
+ model.Session.query(model.resource_revision_table).filter(
+ model.ResourceRevision.id == data_dict['resource_id'],
+ model.ResourceRevision.current is True
+ ).update({'extras': extras}, synchronize_session=False)
+
+ model.Session.commit()
+
+ # get package with updated resource from solr
+ # find changed resource, patch it and reindex package
+ psi = search.PackageSearchIndex()
+ solr_query = search.PackageSearchQuery()
+ q = {
+ 'q': 'id:"{0}"'.format(package_id),
+ 'fl': 'data_dict',
+ 'wt': 'json',
+ 'fq': 'site_id:"%s"' % config.get('ckan.site_id'),
+ 'rows': 1
+ }
+ for record in solr_query.run(q)['results']:
+ solr_data_dict = json.loads(record['data_dict'])
+ for resource in solr_data_dict['resources']:
+ if resource['id'] == data_dict['resource_id']:
+ resource.update(update_dict)
+ psi.index_package(solr_data_dict)
+ break
+
+
def _resource_exists(context, data_dict):
''' Returns true if the resource exists in CKAN and in the datastore '''
model = _get_or_bust(context, 'model')
diff --git a/ckanext/datastore/logic/schema.py b/ckanext/datastore/logic/schema.py
index 11eb54d3a5e..d2eecc4caf9 100644
--- a/ckanext/datastore/logic/schema.py
+++ b/ckanext/datastore/logic/schema.py
@@ -102,7 +102,8 @@ def datastore_create_schema():
'aliases': [ignore_missing, list_of_strings_or_string],
'fields': {
'id': [not_empty, unicode],
- 'type': [ignore_missing]
+ 'type': [ignore_missing],
+ 'info': [ignore_missing],
},
'primary_key': [ignore_missing, list_of_strings_or_string],
'indexes': [ignore_missing, list_of_strings_or_string],
diff --git a/ckanext/datastore/plugin.py b/ckanext/datastore/plugin.py
index 19335a2b884..dc35bd96f28 100644
--- a/ckanext/datastore/plugin.py
+++ b/ckanext/datastore/plugin.py
@@ -56,6 +56,7 @@ class DatastorePlugin(p.SingletonPlugin):
p.implements(p.IDomainObjectModification, inherit=True)
p.implements(p.IRoutes, inherit=True)
p.implements(p.IResourceController, inherit=True)
+ p.implements(p.ITemplateHelpers)
p.implements(interfaces.IDatastore, inherit=True)
legacy_mode = False
@@ -247,9 +248,14 @@ def get_auth_functions(self):
'datastore_change_permissions': auth.datastore_change_permissions}
def before_map(self, m):
- m.connect('/datastore/dump/{resource_id}',
- controller='ckanext.datastore.controller:DatastoreController',
- action='dump')
+ m.connect(
+ '/datastore/dump/{resource_id}',
+ controller='ckanext.datastore.controller:DatastoreController',
+ action='dump')
+ m.connect(
+ 'resource_dictionary', '/dataset/{id}/dictionary/{resource_id}',
+ controller='ckanext.datastore.controller:DatastoreController',
+ action='dictionary', ckan_icon='book')
return m
# IResourceController
@@ -534,3 +540,7 @@ def _ts_rank_alias(self, field=None):
if field:
rank_alias += u' ' + field
return u'"{0}"'.format(rank_alias)
+
+ def get_helpers(self):
+ return {
+ 'datastore_dictionary': datastore_helpers.datastore_dictionary}
diff --git a/ckanext/datastore/templates/datastore/dictionary.html b/ckanext/datastore/templates/datastore/dictionary.html
new file mode 100644
index 00000000000..567904275d3
--- /dev/null
+++ b/ckanext/datastore/templates/datastore/dictionary.html
@@ -0,0 +1,29 @@
+{% extends "package/resource_edit_base.html" %}
+
+{% import 'macros/form.html' as form %}
+
+{% block subtitle %}{{ h.dataset_display_name(pkg) }} - {{ h.resource_display_name(res) }}{% endblock %}
+
+{% block primary_content_inner %}
+
+ {% set action = h.url_for(
+ controller='ckanext.datastore.controller:DatastoreController',
+ action='dictionary', id=pkg.name, resource_id=res.id) %}
+
+
+{% endblock %}
diff --git a/ckanext/datastore/templates/package/resource_edit_base.html b/ckanext/datastore/templates/package/resource_edit_base.html
new file mode 100644
index 00000000000..58332cd7b77
--- /dev/null
+++ b/ckanext/datastore/templates/package/resource_edit_base.html
@@ -0,0 +1,8 @@
+{% ckan_extends %}
+
+{% block inner_primary_nav %}
+ {{ super() }}
+ {% if res.datastore_active %}
+ {{ h.build_nav_icon('resource_dictionary', _('Data Dictionary'), id=pkg.name, resource_id=res.id) }}
+ {% endif %}
+{% endblock %}
diff --git a/ckanext/datastore/templates/package/resource_read.html b/ckanext/datastore/templates/package/resource_read.html
index 636568e9cb4..1a17ae88947 100644
--- a/ckanext/datastore/templates/package/resource_read.html
+++ b/ckanext/datastore/templates/package/resource_read.html
@@ -2,5 +2,36 @@
{% block resource_actions_inner %}
{{ super() }}
- {% snippet 'package/snippets/data_api_button.html', resource=res, datastore_root_url=c.datastore_api %}
-{% endblock %}
\ No newline at end of file
+ {% if res.datastore_active %}
+ {% snippet 'package/snippets/data_api_button.html', resource=res, datastore_root_url=c.datastore_api %}
+ {% endif %}
+{% endblock %}
+
+{% block resource_additional_information_inner %}
+ {% if res.datastore_active %}
+ {% set ddict=h.datastore_dictionary(res.id) %}
+
+
{{ _('Data Dictionary') }}
+
+
+
+ {{ _('Column') }} |
+ {{ _('Type') }} |
+ {{ _('Label') }} |
+ {{ _('Description') }} |
+
+
+ {% for f in ddict %}
+
+ {{ f.id }} |
+ {{ f.type }} |
+ {{ h.get_translated(f.get('info', {}), 'label') }} |
+ {{ h.render_markdown(
+ h.get_translated(f.get('info', {}), 'notes')) }} |
+
+ {% endfor %}
+
+
+ {% endif %}
+ {{ super() }}
+{% endblock %}
diff --git a/doc/maintaining/installing/install-from-source.rst b/doc/maintaining/installing/install-from-source.rst
index 71c3b23f5c3..c022946a9e7 100644
--- a/doc/maintaining/installing/install-from-source.rst
+++ b/doc/maintaining/installing/install-from-source.rst
@@ -40,7 +40,7 @@ pip `A tool for installing and managing Python packages `_
Git `A distributed version control system `_
Apache Solr `A search platform `_
-Jetty `An HTTP server `_ (used for Solr).
+Jetty `An HTTP server `_ (used for Solr).
OpenJDK 6 JDK `The Java Development Kit `_
Redis `An in-memory data structure store `_
===================== ===============================================