diff --git a/CHANGELOG.rst b/CHANGELOG.rst index febaec04c80..08988dbf37d 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -10,6 +10,30 @@ Changelog v.2.9.0 TBA ================== + * When upgrading from previous CKAN versions, the Activity Stream needs a + migrate_package_activity.py running for displaying the history of dataset + changes. This can be performed while CKAN is running or stopped (whereas the + standard `paster db upgrade` migrations need CKAN to be stopped). Ideally it + is run before CKAN is upgraded, but it can be run afterwards. If running + previous versions or this version of CKAN, download and run + migrate_package_activity.py like this: + + cd /usr/lib/ckan/default/src/ckan/ + wget https://raw.githubusercontent.com/ckan/ckan/3484_revision_ui_removal2/ckan/migration/migrate_package_activity.py + wget https://raw.githubusercontent.com/ckan/ckan/3484_revision_ui_removal2/ckan/migration/revision_legacy_code.py + python migrate_package_activity.py -c /etc/ckan/production.ini + + Future versions of CKAN are likely to need a slightly different procedure. + Full info about this migration is found here: + https://github.com/ckan/ckan/wiki/Migrate-package-activity + + * A full history of dataset changes is now displayed in the Activity Stream to + admins, and optionally to the public. By default this is enabled for new + installs, but disabled for sites which upgrade (just in case the history is + sensitive). When upgrading, open data CKANs are encouraged to make this + history open to the public, by setting this in production.ini: + ``ckan.auth.public_activity_stream_detail = true`` (#3972) + Minor changes: * For navl schemas, the 'default' validator no longer applies the default when @@ -26,7 +50,18 @@ Bugfixes: * Action function "datastore_search" would calculate the total, even if you set include_total=False (#4448) -Deprecations: +Removals and deprecations: + + * Revision and History UI is removed: `/revision/*` & `/dataset/{id}/history` + in favour of `/dataset/changes/` visible in the Activity Stream. (#3972) + * Logic functions removed: + ``dashboard_activity_list_html`` ``organization_activity_list_html`` + ``user_activity_list_html`` ``package_activity_list_html`` + ``group_activity_list_html`` ``organization_activity_list_html`` + ``recently_changed_packages_activity_list_html`` + ``dashboard_activity_list_html`` ``activity_detail_list`` (#4627/#3972) + * ``model.ActivityDetail`` is no longer used and will be removed in the next + CKAN release. (#3972) * ``c.action`` and ``c.controller`` variables should be avoided. ``ckan.plugins.toolkit.get_endpoint`` can be used instead. This function returns tuple of two items(depending on request handler): diff --git a/ckan/authz.py b/ckan/authz.py index 73aa0e6f45c..5ac81f0229c 100644 --- a/ckan/authz.py +++ b/ckan/authz.py @@ -407,6 +407,7 @@ def get_user_id_for_username(user_name, allow_none=False): 'create_user_via_api': False, 'create_user_via_web': True, 'roles_that_cascade_to_sub_groups': 'admin', + 'public_activity_stream_detail': False, } diff --git a/ckan/config/deployment.ini_tmpl b/ckan/config/deployment.ini_tmpl index 741c9f89a91..0ed97f60336 100644 --- a/ckan/config/deployment.ini_tmpl +++ b/ckan/config/deployment.ini_tmpl @@ -72,6 +72,8 @@ ckan.auth.user_delete_organizations = true ckan.auth.create_user_via_api = false ckan.auth.create_user_via_web = true ckan.auth.roles_that_cascade_to_sub_groups = admin +ckan.auth.public_user_details = true +ckan.auth.public_activity_stream_detail = true ## Search Settings diff --git a/ckan/config/routing.py b/ckan/config/routing.py index fb771ea76be..0332214fb97 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -134,13 +134,6 @@ def make_map(): # users map.redirect('/users/{url:.*}', '/user/{url}') - with SubMapper(map, controller='revision') as m: - m.connect('/revision', action='index') - m.connect('/revision/edit/{id}', action='edit') - m.connect('/revision/diff/{id}', action='diff') - m.connect('/revision/list', action='list') - m.connect('/revision/{id}', action='read') - with SubMapper(map, controller='util') as m: m.connect('/i18n/strings_{lang}.js', action='i18n_js_strings') m.connect('/util/redirect', action='redirect') diff --git a/ckan/controllers/revision.py b/ckan/controllers/revision.py deleted file mode 100644 index 7b5f94043ef..00000000000 --- a/ckan/controllers/revision.py +++ /dev/null @@ -1,185 +0,0 @@ -# encoding: utf-8 - -from datetime import datetime, timedelta - -from pylons.i18n import get_lang -from six import text_type - -import ckan.logic as logic -import ckan.lib.base as base -import ckan.model as model -import ckan.lib.helpers as h - -from ckan.common import _, c, request - - -class RevisionController(base.BaseController): - - def __before__(self, action, **env): - base.BaseController.__before__(self, action, **env) - - context = {'model': model, 'user': c.user, - 'auth_user_obj': c.userobj} - if c.user: - try: - logic.check_access('revision_change_state', context) - c.revision_change_state_allowed = True - except logic.NotAuthorized: - c.revision_change_state_allowed = False - else: - c.revision_change_state_allowed = False - try: - logic.check_access('site_read', context) - except logic.NotAuthorized: - base.abort(403, _('Not authorized to see this page')) - - def index(self): - return self.list() - - def list(self): - format = request.params.get('format', '') - if format == 'atom': - # Generate and return Atom 1.0 document. - from webhelpers.feedgenerator import Atom1Feed - feed = Atom1Feed( - title=_(u'CKAN Repository Revision History'), - link=h.url_for(controller='revision', action='list', id=''), - description=_(u'Recent changes to the CKAN repository.'), - language=text_type(get_lang()), - ) - # TODO: make this configurable? - # we do not want the system to fall over! - maxresults = 200 - try: - dayHorizon = int(request.params.get('days', 5)) - except: - dayHorizon = 5 - ourtimedelta = timedelta(days=-dayHorizon) - since_when = datetime.now() + ourtimedelta - revision_query = model.repo.history() - revision_query = revision_query.filter( - model.Revision.timestamp >= since_when).filter( - model.Revision.id != None) - revision_query = revision_query.limit(maxresults) - for revision in revision_query: - package_indications = [] - revision_changes = model.repo.list_changes(revision) - resource_revisions = revision_changes[model.Resource] - for package in revision.packages: - if not package: - # package is None sometimes - I don't know why, - # but in the meantime while that is fixed, - # avoid an exception here - continue - if package.private: - continue - number = len(package.all_revisions) - package_revision = None - count = 0 - for pr in package.all_revisions: - count += 1 - if pr.revision.id == revision.id: - package_revision = pr - break - if package_revision and package_revision.state == \ - model.State.DELETED: - transition = 'deleted' - elif package_revision and count == number: - transition = 'created' - else: - transition = 'updated' - for resource_revision in resource_revisions: - if resource_revision.package_id == package.id: - transition += ':resources' - break - indication = "%s:%s" % (package.name, transition) - package_indications.append(indication) - pkgs = u'[%s]' % ' '.join(package_indications) - item_title = u'r%s ' % (revision.id) - item_title += pkgs - if revision.message: - item_title += ': %s' % (revision.message or '') - item_link = h.url_for(controller='revision', action='read', id=revision.id) - item_description = _('Datasets affected: %s.\n') % pkgs - item_description += '%s' % (revision.message or '') - item_author_name = revision.author - item_pubdate = revision.timestamp - feed.add_item( - title=item_title, - link=item_link, - description=item_description, - author_name=item_author_name, - pubdate=item_pubdate, - ) - feed.content_type = 'application/atom+xml' - return feed.writeString('utf-8') - else: - query = model.Session.query(model.Revision) - c.page = h.Page( - collection=query, - page=h.get_page_number(request.params), - url=h.pager_url, - items_per_page=20 - ) - return base.render('revision/list.html') - - def read(self, id=None): - if id is None: - base.abort(404) - c.revision = model.Session.query(model.Revision).get(id) - if c.revision is None: - base.abort(404) - - pkgs = model.Session.query(model.PackageRevision).\ - filter_by(revision=c.revision) - c.packages = [pkg.continuity for pkg in pkgs if not pkg.private] - pkgtags = model.Session.query(model.PackageTagRevision).\ - filter_by(revision=c.revision) - c.pkgtags = [pkgtag.continuity for pkgtag in pkgtags - if not pkgtag.package.private] - grps = model.Session.query(model.GroupRevision).\ - filter_by(revision=c.revision) - c.groups = [grp.continuity for grp in grps] - return base.render('revision/read.html') - - def diff(self, id=None): - if 'diff' not in request.params or 'oldid' not in request.params: - base.abort(400) - c.revision_from = model.Session.query(model.Revision).get( - request.params.getone('oldid')) - c.revision_to = model.Session.query(model.Revision).get( - request.params.getone('diff')) - - c.diff_entity = request.params.get('diff_entity') - if c.diff_entity == 'package': - c.pkg = model.Package.by_name(id) - diff = c.pkg.diff(c.revision_to, c.revision_from) - elif c.diff_entity == 'group': - c.group = model.Group.by_name(id) - diff = c.group.diff(c.revision_to, c.revision_from) - else: - base.abort(400) - - c.diff = diff.items() - c.diff.sort() - return base.render('revision/diff.html') - - def edit(self, id=None): - if id is None: - base.abort(404) - revision = model.Session.query(model.Revision).get(id) - if revision is None: - base.abort(404) - action = request.params.get('action', '') - if action in ['delete', 'undelete']: - # this should be at a lower level (e.g. logic layer) - if not c.revision_change_state_allowed: - base.abort(403) - if action == 'delete': - revision.state = model.State.DELETED - elif action == 'undelete': - revision.state = model.State.ACTIVE - model.Session.commit() - h.flash_success(_('Revision updated')) - h.redirect_to( - h.url_for(controller='revision', action='read', id=id)) diff --git a/ckan/lib/activity_streams_session_extension.py b/ckan/lib/activity_streams_session_extension.py index 85cf677f948..3dec8a95173 100644 --- a/ckan/lib/activity_streams_session_extension.py +++ b/ckan/lib/activity_streams_session_extension.py @@ -18,30 +18,22 @@ def activity_stream_item(obj, activity_type, revision, user_id): return None -def activity_stream_detail(obj, activity_id, activity_type): - method = getattr(obj, "activity_stream_detail", None) - if callable(method): - return method(activity_id, activity_type) - else: - # Object did not have a suitable activity_stream_detail() method - return None - - class DatasetActivitySessionExtension(SessionExtension): """Session extension that emits activity stream activities for packages and related objects. An SQLAlchemy SessionExtension that watches for new, changed or deleted Packages or objects with related packages (Resources, PackageExtras..) - being committed to the SQLAlchemy session and creates Activity and - ActivityDetail objects for these activities. + being committed to the SQLAlchemy session and creates Activity objects for + these activities. - For most types of activity the Activity and ActivityDetail objects are - created in the relevant ckan/logic/action/ functions, but for Packages and - objects with related packages they are created by this class instead. + For most types of activity the Activity objects are created in the relevant + ckan/logic/action/ functions, but for Packages and objects with related + packages they are created by this class instead. """ def before_commit(self, session): + from ckan.model import Member # imported here to avoid dependency hell if not asbool(config.get('ckan.activity_streams_enabled', 'true')): return @@ -67,12 +59,6 @@ def before_commit(self, session): # objects. activities = {} - # The second-level objects that we will append to the activity_detail - # table. Each row in the activity table has zero or more related rows - # in the activity_detail table. The keys here are activity IDs, and the - # values are lists of model.activity:ActivityDetail objects. - activity_details = {} - # Log new packages first to prevent them from getting incorrectly # logged as changed packages. # Looking for new packages... @@ -89,10 +75,6 @@ def before_commit(self, session): activities[obj.id] = activity - activity_detail = activity_stream_detail(obj, activity.id, "new") - if activity_detail is not None: - activity_details[activity.id] = [activity_detail] - # Now process other objects. for activity_type in ('new', 'changed', 'deleted'): objects = object_cache[activity_type] @@ -114,6 +96,11 @@ def before_commit(self, session): # skipping it continue + if isinstance(obj, Member): + # When you add a package to a group/org, it should only be + # in the group's activity stream, not the related packages + continue + for package in related_packages: if package is None: continue @@ -123,31 +110,16 @@ def before_commit(self, session): continue if package.id in activities: - activity = activities[package.id] - else: - activity = activity_stream_item( - package, "changed", revision, user_id) - if activity is None: - continue - - activity_detail = activity_stream_detail( - obj, activity.id, activity_type) - if activity_detail is not None: - if not package.id in activities: - activities[package.id] = activity - if activity_details.has_key(activity.id): - activity_details[activity.id].append( - activity_detail) - else: - activity_details[activity.id] = [activity_detail] + continue + + activity = activity_stream_item( + package, "changed", revision, user_id) + if activity is None: + continue + activities[package.id] = activity for key, activity in activities.items(): # Emitting activity session.add(activity) - for key, activity_detail_list in activity_details.items(): - for activity_detail_obj in activity_detail_list: - # Emitting activity detail - session.add(activity_detail_obj) - session.flush() diff --git a/ckan/lib/app_globals.py b/ckan/lib/app_globals.py index ce545dd288e..7e412d10f79 100644 --- a/ckan/lib/app_globals.py +++ b/ckan/lib/app_globals.py @@ -47,7 +47,6 @@ 'ckan.template_footer_end': {}, 'ckan.dumps_url': {}, 'ckan.dumps_format': {}, - 'ofs.impl': {'name': 'ofs_impl'}, 'ckan.homepage_style': {'default': '1'}, # split string diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 15f1fd62225..1831b699ea1 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -9,13 +9,13 @@ import inspect import sys - from pylons import cache from pylons.controllers import WSGIController from pylons.controllers.util import abort as _abort from pylons.decorators import jsonify from pylons.templating import cached_template, pylons_globals from webhelpers.html import literal +from jinja2.exceptions import TemplateNotFound from flask import ( render_template as flask_render_template, @@ -76,17 +76,37 @@ def abort(status_code=None, detail='', headers=None, comment=None): comment=comment) -def render_snippet(template_name, **kw): +def render_snippet(*template_names, **kw): ''' Helper function for rendering snippets. Rendered html has comment tags added to show the template used. NOTE: unlike other render functions this takes a list of keywords instead of a dict for - the extra template variables. ''' + the extra template variables. + + :param template_names: the template to render, optionally with fallback + values, for when the template can't be found. For each, specify the + relative path to the template inside the registered tpl_dir. + :type template_names: str + :param kw: extra template variables to supply to the template + :type kw: named arguments of any type that are supported by the template + ''' - output = render(template_name, extra_vars=kw) - if config.get('debug'): - output = ('\n\n%s\n\n' - % (template_name, output, template_name)) - return literal(output) + exc = None + for template_name in template_names: + try: + output = render(template_name, extra_vars=kw) + if config.get('debug'): + output = ( + '\n\n%s\n' + '\n' % (template_name, output, template_name)) + return literal(output) + except TemplateNotFound as exc: + if exc.name == template_name: + # the specified template doesn't exist - try the next fallback + continue + # a nested template doesn't exist - don't fallback + raise exc + else: + raise exc or TemplateNotFound def render_jinja2(template_name, extra_vars): diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index f7a1e145f08..14753410f65 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -133,83 +133,35 @@ def _execute(q, table, context): return session.execute(q) -def _execute_with_revision(q, rev_table, context): - ''' - Takes an SqlAlchemy query (q) that is (at its base) a Select on an object - revision table (rev_table), and you provide revision_id or revision_date in - the context and it will filter the object revision(s) to an earlier time. - - Raises NotFound if context['revision_id'] is provided, but the revision - ID does not exist. - - Returns [] if there are no results. - - ''' - model = context['model'] - session = model.Session - revision_id = context.get('revision_id') - revision_date = context.get('revision_date') - - if revision_id: - revision = session.query(context['model'].Revision).filter_by( - id=revision_id).first() - if not revision: - raise logic.NotFound - revision_date = revision.timestamp - - q = q.where(rev_table.c.revision_timestamp <= revision_date) - q = q.where(rev_table.c.expired_timestamp > revision_date) - - return session.execute(q) - - def package_dictize(pkg, context): ''' Given a Package object, returns an equivalent dictionary. - - Normally this is the most recent version, but you can provide revision_id - or revision_date in the context and it will filter to an earlier time. - - May raise NotFound if: - * the specified revision_id doesn't exist - * the specified revision_date was before the package was created ''' model = context['model'] - is_latest_revision = not(context.get('revision_id') or - context.get('revision_date')) - execute = _execute if is_latest_revision else _execute_with_revision - #package - if is_latest_revision: - if isinstance(pkg, model.PackageRevision): - pkg = model.Package.get(pkg.id) - result = pkg - else: - package_rev = model.package_revision_table - q = select([package_rev]).where(package_rev.c.id == pkg.id) - result = execute(q, package_rev, context).first() - if not result: + assert not (context.get('revision_id') or + context.get('revision_date')), \ + 'Revision functionality is moved to migrate_package_activity' + assert not isinstance(pkg, model.PackageRevision), \ + 'Revision functionality is moved to migrate_package_activity' + execute = _execute + # package + if not pkg: raise logic.NotFound - result_dict = d.table_dictize(result, context) - #strip whitespace from title + result_dict = d.table_dictize(pkg, context) + # strip whitespace from title if result_dict.get('title'): result_dict['title'] = result_dict['title'].strip() - #resources - if is_latest_revision: - res = model.resource_table - else: - res = model.resource_revision_table + # resources + res = model.resource_table q = select([res]).where(res.c.package_id == pkg.id) result = execute(q, res, context) result_dict["resources"] = resource_list_dictize(result, context) result_dict['num_resources'] = len(result_dict.get('resources', [])) - #tags + # tags tag = model.tag_table - if is_latest_revision: - pkg_tag = model.package_tag_table - else: - pkg_tag = model.package_tag_revision_table + pkg_tag = model.package_tag_table q = select([tag, pkg_tag.c.state], from_obj=pkg_tag.join(tag, tag.c.id == pkg_tag.c.tag_id) ).where(pkg_tag.c.package_id == pkg.id) @@ -222,20 +174,17 @@ def package_dictize(pkg, context): # same as its name, but the display_name might get changed later (e.g. # translated into another language by the multilingual extension). for tag in result_dict['tags']: - assert not 'display_name' in tag + assert 'display_name' not in tag tag['display_name'] = tag['name'] - #extras - no longer revisioned, so always provide latest + # extras - no longer revisioned, so always provide latest extra = model.package_extra_table q = select([extra]).where(extra.c.package_id == pkg.id) result = _execute(q, extra, context) result_dict["extras"] = extras_list_dictize(result, context) - #groups - if is_latest_revision: - member = model.member_table - else: - member = model.member_revision_table + # groups + member = model.member_table group = model.group_table q = select([group, member.c.capacity], from_obj=member.join(group, group.c.id == member.c.group_id) @@ -244,17 +193,14 @@ def package_dictize(pkg, context): .where(group.c.is_organization == False) result = execute(q, member, context) context['with_capacity'] = False - ## no package counts as cannot fetch from search index at the same - ## time as indexing to it. - ## tags, extras and sub-groups are not included for speed + # no package counts as cannot fetch from search index at the same + # time as indexing to it. + # tags, extras and sub-groups are not included for speed result_dict["groups"] = group_list_dictize(result, context, with_package_counts=False) - #owning organization - if is_latest_revision: - group = model.group_table - else: - group = model.group_revision_table + # owning organization + group = model.group_table q = select([group] ).where(group.c.id == pkg.owner_org) \ .where(group.c.state == 'active') @@ -265,11 +211,8 @@ def package_dictize(pkg, context): else: result_dict["organization"] = None - #relations - if is_latest_revision: - rel = model.package_relationship_table - else: - rel = model.package_relationship_revision_table + # relations + rel = model.package_relationship_table q = select([rel]).where(rel.c.subject_package_id == pkg.id) result = execute(q, rel, context) result_dict["relationships_as_subject"] = \ @@ -280,13 +223,10 @@ def package_dictize(pkg, context): d.obj_list_dictize(result, context) # Extra properties from the domain object - # We need an actual Package object for this, not a PackageRevision - if isinstance(pkg, model.PackageRevision): - pkg = model.Package.get(pkg.id) # isopen result_dict['isopen'] = pkg.isopen if isinstance(pkg.isopen, bool) \ - else pkg.isopen() + else pkg.isopen() # type # if null assign the default value to make searching easier @@ -693,19 +633,23 @@ def vocabulary_list_dictize(vocabulary_list, context): return [vocabulary_dictize(vocabulary, context) for vocabulary in vocabulary_list] -def activity_dictize(activity, context): +def activity_dictize(activity, context, include_data=False): activity_dict = d.table_dictize(activity, context) + if not include_data: + # replace the data with just a {'title': title} and not the rest of + # the dataset/group/org/custom obj. we need the title to display it + # in the activity stream. + activity_dict['data'] = { + key: {'title': val['title']} + for (key, val) in activity_dict['data'].items() + if isinstance(val, dict) and 'title' in val} return activity_dict -def activity_list_dictize(activity_list, context): - return [activity_dictize(activity, context) for activity in activity_list] - -def activity_detail_dictize(activity_detail, context): - return d.table_dictize(activity_detail, context) -def activity_detail_list_dictize(activity_detail_list, context): - return [activity_detail_dictize(activity_detail, context) - for activity_detail in activity_detail_list] +def activity_list_dictize(activity_list, context, + include_data=False): + return [activity_dictize(activity, context, include_data) + for activity in activity_list] def package_to_api1(pkg, context): diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index a5bfadee831..e11dfdd9134 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -189,7 +189,7 @@ def redirect_to(*args, **kw): _url = '' skip_url_parsing = False parse_url = kw.pop('parse_url', False) - if uargs and len(uargs) is 1 and isinstance(uargs[0], string_types) \ + if uargs and len(uargs) == 1 and isinstance(uargs[0], string_types) \ and (uargs[0].startswith('/') or is_url(uargs[0])) \ and parse_url is False: skip_url_parsing = True diff --git a/ckan/lib/jinja_extensions.py b/ckan/lib/jinja_extensions.py index 82851fd7919..8520e7bd862 100644 --- a/ckan/lib/jinja_extensions.py +++ b/ckan/lib/jinja_extensions.py @@ -255,7 +255,8 @@ def make_call_node(*kw): class SnippetExtension(BaseExtension): ''' Custom snippet tag - {% snippet [, =].. %} + {% snippet [, ]... + [, =]... %} see lib.helpers.snippet() for more details. ''' @@ -264,8 +265,7 @@ class SnippetExtension(BaseExtension): @classmethod def _call(cls, args, kwargs): - assert len(args) == 1 - return base.render_snippet(args[0], **kwargs) + return base.render_snippet(*args, **kwargs) class UrlForStaticExtension(BaseExtension): ''' Custom url_for_static tag for getting a path for static assets. diff --git a/ckan/lib/plugins.py b/ckan/lib/plugins.py index 5cf78b2896a..ae62c6360b4 100644 --- a/ckan/lib/plugins.py +++ b/ckan/lib/plugins.py @@ -358,7 +358,7 @@ def search_template(self): return 'package/search.html' def history_template(self): - return 'package/history.html' + return None def resource_template(self): return 'package/resource_read.html' @@ -417,13 +417,6 @@ def about_template(self): """ return 'group/about.html' - def history_template(self): - """ - Returns a string representing the location of the template to be - rendered for the history page - """ - return 'group/history.html' - def edit_template(self): """ Returns a string representing the location of the template to be diff --git a/ckan/lib/uploader.py b/ckan/lib/uploader.py index 9d5fe84217f..fee5a7ac0a2 100644 --- a/ckan/lib/uploader.py +++ b/ckan/lib/uploader.py @@ -79,20 +79,8 @@ def get_storage_path(): # None means it has not been set. False means not in config. if _storage_path is None: storage_path = config.get('ckan.storage_path') - ofs_impl = config.get('ofs.impl') - ofs_storage_dir = config.get('ofs.storage_dir') if storage_path: _storage_path = storage_path - elif ofs_impl == 'pairtree' and ofs_storage_dir: - log.warn('''Please use config option ckan.storage_path instead of - ofs.storage_dir''') - _storage_path = ofs_storage_dir - return _storage_path - elif ofs_impl: - log.critical('''We only support local file storage form version 2.2 - of ckan please specify ckan.storage_path in your - config for your uploads''') - _storage_path = False else: log.critical('''Please specify a ckan.storage_path in your config for your uploads''') diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index ee3ccedb50b..8a5ca8acdb6 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1003,9 +1003,7 @@ def package_show(context, data_dict): include_tracking = asbool(data_dict.get('include_tracking', False)) package_dict = None - use_cache = (context.get('use_cache', True) - and not 'revision_id' in context - and not 'revision_date' in context) + use_cache = (context.get('use_cache', True)) if use_cache: try: search_result = search.show(name_or_id) @@ -2504,7 +2502,8 @@ def user_activity_list(context, data_dict): ''' # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _check_access('user_show', context, data_dict) + data_dict['include_data'] = False + _check_access('user_activity_list', context, data_dict) model = context['model'] @@ -2521,12 +2520,14 @@ def user_activity_list(context, data_dict): activity_objects = _filter_activity_by_user( _activity_objects, _activity_stream_get_filtered_users()) - return model_dictize.activity_list_dictize(activity_objects, context) + return model_dictize.activity_list_dictize( + activity_objects, context, + include_data=data_dict['include_data']) @logic.validate(logic.schema.default_activity_list_schema) def package_activity_list(context, data_dict): - '''Return a package's activity stream. + '''Return a package's activity stream (not including detail) You must be authorized to view the package. @@ -2540,13 +2541,23 @@ def package_activity_list(context, data_dict): ``ckan.activity_list_limit``, upper limit: ``100`` unless set in site's configuration ``ckan.activity_list_limit_max``) :type limit: int + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool :rtype: list of dictionaries ''' # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _check_access('package_show', context, data_dict) + data_dict['include_data'] = False + include_hidden_activity = data_dict.get('include_hidden_activity', False) + _check_access('package_activity_list', context, data_dict) model = context['model'] @@ -2560,10 +2571,14 @@ def package_activity_list(context, data_dict): _activity_objects = model.activity.package_activity_list( package.id, limit=limit, offset=offset) - activity_objects = _filter_activity_by_user( - _activity_objects, _activity_stream_get_filtered_users()) + if not include_hidden_activity: + activity_objects = _filter_activity_by_user( + _activity_objects, _activity_stream_get_filtered_users()) + else: + activity_objects = _activity_objects - return model_dictize.activity_list_dictize(activity_objects, context) + return model_dictize.activity_list_dictize( + activity_objects, context, include_data=data_dict['include_data']) @logic.validate(logic.schema.default_activity_list_schema) @@ -2582,13 +2597,23 @@ def group_activity_list(context, data_dict): ``ckan.activity_list_limit``, upper limit: ``100`` unless set in site's configuration ``ckan.activity_list_limit_max``) :type limit: int + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool :rtype: list of dictionaries ''' # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _check_access('group_show', context, data_dict) + data_dict = dict(data_dict, include_data=False) + include_hidden_activity = data_dict.get('include_hidden_activity', False) + _check_access('group_activity_list', context, data_dict) model = context['model'] group_id = data_dict.get('id') @@ -2601,10 +2626,15 @@ def group_activity_list(context, data_dict): _activity_objects = model.activity.group_activity_list( group_id, limit=limit, offset=offset) - activity_objects = _filter_activity_by_user( - _activity_objects, _activity_stream_get_filtered_users()) + if not include_hidden_activity: + activity_objects = _filter_activity_by_user( + _activity_objects, _activity_stream_get_filtered_users()) + else: + activity_objects = _activity_objects - return model_dictize.activity_list_dictize(activity_objects, context) + return model_dictize.activity_list_dictize( + activity_objects, context, + include_data=data_dict['include_data']) @logic.validate(logic.schema.default_activity_list_schema) @@ -2621,13 +2651,23 @@ def organization_activity_list(context, data_dict): ``ckan.activity_list_limit``, upper limit: ``100`` unless set in site's configuration ``ckan.activity_list_limit_max``) :type limit: int + :param include_hidden_activity: whether to include 'hidden' activity, which + is not shown in the Activity Stream page. Hidden activity includes + activity done by the site_user, such as harvests, which are not shown + in the activity stream because they can be too numerous, or activity by + other users specified in config option `ckan.hide_activity_from_users`. + NB Only sysadmins may set include_hidden_activity to true. + (default: false) + :type include_hidden_activity: bool :rtype: list of dictionaries ''' # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _check_access('organization_show', context, data_dict) + data_dict['include_data'] = False + include_hidden_activity = data_dict.get('include_hidden_activity', False) + _check_access('organization_activity_list', context, data_dict) model = context['model'] org_id = data_dict.get('id') @@ -2640,10 +2680,15 @@ def organization_activity_list(context, data_dict): _activity_objects = model.activity.organization_activity_list( org_id, limit=limit, offset=offset) - activity_objects = _filter_activity_by_user( - _activity_objects, _activity_stream_get_filtered_users()) + if not include_hidden_activity: + activity_objects = _filter_activity_by_user( + _activity_objects, _activity_stream_get_filtered_users()) + else: + activity_objects = _activity_objects - return model_dictize.activity_list_dictize(activity_objects, context) + return model_dictize.activity_list_dictize( + activity_objects, context, + include_data=data_dict['include_data']) @logic.validate(logic.schema.default_dashboard_activity_list_schema) @@ -2666,31 +2711,19 @@ def recently_changed_packages_activity_list(context, data_dict): # authorized to read. model = context['model'] offset = data_dict.get('offset', 0) + data_dict['include_data'] = False limit = data_dict['limit'] # defaulted, limited & made an int by schema - _activity_objects = model.activity.recently_changed_packages_activity_list( - limit=limit, offset=offset) + _activity_objects = \ + model.activity.recently_changed_packages_activity_list( + limit=limit, offset=offset) activity_objects = _filter_activity_by_user( - _activity_objects, _activity_stream_get_filtered_users()) - - return model_dictize.activity_list_dictize(activity_objects, context) - - -def activity_detail_list(context, data_dict): - '''Return an activity's list of activity detail items. + _activity_objects, + _activity_stream_get_filtered_users()) - :param id: the id of the activity - :type id: string - :rtype: list of dictionaries. - - ''' - # FIXME: Filter out activities whose subject or object the user is not - # authorized to read. - model = context['model'] - activity_id = _get_or_bust(data_dict, 'id') - activity_detail_objects = model.ActivityDetail.by_activity_id(activity_id) - return model_dictize.activity_detail_list_dictize( - activity_detail_objects, context) + return model_dictize.activity_list_dictize( + activity_objects, context, + include_data=data_dict['include_data']) def _follower_count(context, data_dict, default_schema, ModelClass): @@ -3199,13 +3232,13 @@ def dashboard_activity_list(context, data_dict): # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _activity_objects = model.activity.dashboard_activity_list( + _activity_tuple_objects = model.activity.dashboard_activity_list( user_id, limit=limit, offset=offset) - activity_objects = _filter_activity_by_user( - _activity_objects, _activity_stream_get_filtered_users()) + activity_tuple_list = _filter_activity_by_user( + _activity_tuple_objects, _activity_stream_get_filtered_users()) activity_dicts = model_dictize.activity_list_dictize( - activity_objects, context) + activity_tuple_list, context) # Mark the new (not yet seen by user) activities. strptime = datetime.datetime.strptime @@ -3241,6 +3274,146 @@ def dashboard_new_activities_count(context, data_dict): return len([activity for activity in activities if activity['is_new']]) +def activity_show(context, data_dict): + '''Show details of an item of 'activity' (part of the activity stream). + + :param id: the id of the activity + :type id: string + :param include_data: include the data field, containing a full object dict + (otherwise the data field is only returned with the object's title) + :type include_data: boolean + + :rtype: dictionary + ''' + model = context['model'] + user = context['user'] + activity_id = _get_or_bust(data_dict, 'id') + include_data = asbool(_get_or_bust(data_dict, 'include_data')) + + activity = model.Session.query(model.Activity).get(activity_id) + if activity is None: + raise NotFound + context['activity'] = activity + + _check_access(u'activity_show', context, data_dict) + + activity = model_dictize.activity_dictize(activity, context, + include_data=include_data) + return activity + + +def activity_data_show(context, data_dict): + '''Show the data from an item of 'activity' (part of the activity + stream). + + For example for a package update this returns just the dataset dict but + none of the activity stream info of who and when the version was created. + + :param id: the id of the activity + :type id: string + :param object_type: 'package', 'user', 'group' or 'organization' + :type object_type: string + + :rtype: dictionary + ''' + model = context['model'] + user = context['user'] + activity_id = _get_or_bust(data_dict, 'id') + object_type = data_dict.get('object_type') + + activity = model.Session.query(model.Activity).get(activity_id) + if activity is None: + raise NotFound + context['activity'] = activity + + _check_access(u'activity_data_show', context, data_dict) + + activity = model_dictize.activity_dictize(activity, context, + include_data=True) + try: + activity_data = activity['data'] + except KeyError: + raise NotFound('Could not find data in the activity') + if object_type: + try: + activity_data = activity_data[object_type] + except KeyError: + raise NotFound('Could not find that object_type in the activity') + return activity_data + + +def activity_diff(context, data_dict): + '''Returns a diff of the activity, compared to the previous version of the + object + + :param id: the id of the activity + :type id: string + :param object_type: 'package', 'user', 'group' or 'organization' + :type object_type: string + :param diff_type: 'unified', 'context', 'html' + :type diff_type: string + ''' + import difflib + from pprint import pformat + + model = context['model'] + user = context['user'] + activity_id = _get_or_bust(data_dict, 'id') + object_type = _get_or_bust(data_dict, 'object_type') + diff_type = data_dict.get('diff_type', 'unified') + + _check_access(u'activity_diff', context, data_dict) + + activity = model.Session.query(model.Activity).get(activity_id) + if activity is None: + raise NotFound + prev_activity = model.Session.query(model.Activity) \ + .filter_by(object_id=activity.object_id) \ + .filter(model.Activity.timestamp < activity.timestamp) \ + .order_by(model.Activity.timestamp.desc()) \ + .first() + if prev_activity is None: + raise NotFound('Previous activity for this object not found') + activity_objs = [prev_activity, activity] + try: + objs = [activity_obj.data[object_type] + for activity_obj in activity_objs] + except KeyError: + raise NotFound('Could not find object in the activity data') + # convert each object dict to 'pprint'-style + # and split into lines to suit difflib + obj_lines = [json.dumps(obj, indent=2, sort_keys=True).split('\n') + for obj in objs] + + # do the diff + if diff_type == 'unified': + diff_generator = difflib.unified_diff(*obj_lines) + diff = '\n'.join(line for line in diff_generator) + elif diff_type == 'context': + diff_generator = difflib.context_diff(*obj_lines) + diff = '\n'.join(line for line in diff_generator) + elif diff_type == 'html': + # word-wrap lines. Otherwise you get scroll bars for most datasets. + import re + for obj_index in (0, 1): + wrapped_obj_lines = [] + for line in obj_lines[obj_index]: + wrapped_obj_lines.extend(re.findall(r'.{1,70}(?:\s+|$)', line)) + obj_lines[obj_index] = wrapped_obj_lines + diff = difflib.HtmlDiff().make_table(*obj_lines) + else: + raise ValidationError('diff_type not recognized') + + activities = [model_dictize.activity_dictize(activity_obj, context, + include_data=True) + for activity_obj in activity_objs] + + return { + 'diff': diff, + 'activities': activities, + } + + def _unpick_search(sort, allowed_fields=None, total=None): ''' This is a helper function that takes a sort string eg 'name asc, last_modified desc' and returns a list of diff --git a/ckan/logic/auth/__init__.py b/ckan/logic/auth/__init__.py index 7556f0441de..f8ba55ca7cd 100644 --- a/ckan/logic/auth/__init__.py +++ b/ckan/logic/auth/__init__.py @@ -44,6 +44,10 @@ def get_user_object(context, data_dict=None): return _get_object(context, data_dict, 'user_obj', 'User') +def get_activity_object(context, data_dict=None): + return _get_object(context, data_dict, 'activity', 'Activity') + + def restrict_anon(context): if authz.auth_is_anon_user(context): return {'success': False} diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index aef7364c349..4561e6547ab 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -4,7 +4,8 @@ import ckan.authz as authz from ckan.common import _, config from ckan.logic.auth import (get_package_object, get_group_object, - get_resource_object, restrict_anon) + get_resource_object, get_activity_object, + restrict_anon) from ckan.lib.plugins import get_permission_labels from paste.deploy.converters import asbool @@ -270,6 +271,94 @@ def dashboard_new_activities_count(context, data_dict): context, data_dict) +def activity_list(context, data_dict): + ''' + :param id: the id or name of the object (e.g. package id) + :type id: string + :param object_type: The type of the object (e.g. 'package', 'organization', + 'group', 'user') + :type object_type: string + :param include_data: include the data field, containing a full object dict + (otherwise the data field is only returned with the object's title) + :type include_data: boolean + ''' + if data_dict['object_type'] not in ('package', 'organization', 'group', + 'user'): + return {'success': False, 'msg': 'object_type not recognized'} + if (data_dict.get('include_data') and + not authz.check_config_permission('public_activity_stream_detail')): + # The 'data' field of the activity is restricted to users who are + # allowed to edit the object + show_or_update = 'update' + else: + # the activity for an object (i.e. the activity metadata) can be viewed + # if the user can see the object + show_or_update = 'show' + action_on_which_to_base_auth = '{}_{}'.format( + data_dict['object_type'], show_or_update) # e.g. 'package_update' + return authz.is_authorized(action_on_which_to_base_auth, context, + {'id': data_dict['id']}) + + +def user_activity_list(context, data_dict): + data_dict['object_type'] = 'user' + return activity_list(context, data_dict) + + +def package_activity_list(context, data_dict): + data_dict['object_type'] = 'package' + return activity_list(context, data_dict) + + +def group_activity_list(context, data_dict): + data_dict['object_type'] = 'group' + return activity_list(context, data_dict) + + +def organization_activity_list(context, data_dict): + data_dict['object_type'] = 'organization' + return activity_list(context, data_dict) + + +def activity_show(context, data_dict): + ''' + :param id: the id of the activity + :type id: string + :param include_data: include the data field, containing a full object dict + (otherwise the data field is only returned with the object's title) + :type include_data: boolean + ''' + activity = get_activity_object(context, data_dict) + # NB it would be better to have recorded an activity_type against the + # activity + if 'package' in activity.activity_type: + object_type = 'package' + else: + return {'success': False, 'msg': 'object_type not recognized'} + return activity_list(context, { + 'id': activity.object_id, + 'include_data': data_dict['include_data'], + 'object_type': object_type}) + + +def activity_data_show(context, data_dict): + ''' + :param id: the id of the activity + :type id: string + ''' + data_dict['include_data'] = True + return activity_show(context, data_dict) + + +def activity_diff(context, data_dict): + ''' + :param id: the id of the activity + :type id: string + ''' + data_dict['include_data'] = True + return activity_show(context, data_dict) + + def user_follower_list(context, data_dict): return authz.is_authorized('sysadmin', context, data_dict) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 7aa77fdd21d..8ba032dc899 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -615,13 +615,16 @@ def default_dashboard_activity_list_schema( @validator_args def default_activity_list_schema( not_missing, unicode_safe, configured_default, - natural_number_validator, limit_to_configured_maximum): + natural_number_validator, limit_to_configured_maximum, + ignore_missing, boolean_validator, ignore_not_sysadmin): schema = default_pagination_schema() schema['id'] = [not_missing, unicode_safe] schema['limit'] = [ configured_default('ckan.activity_list_limit', 31), natural_number_validator, limit_to_configured_maximum('ckan.activity_list_limit_max', 100)] + schema['include_hidden_activity'] = [ + ignore_missing, ignore_not_sysadmin, boolean_validator] return schema diff --git a/ckan/migration/migrate_package_activity.py b/ckan/migration/migrate_package_activity.py new file mode 100644 index 00000000000..e08c8dc132f --- /dev/null +++ b/ckan/migration/migrate_package_activity.py @@ -0,0 +1,293 @@ +# encoding: utf-8 + +''' +Migrates revisions into the activity stream, to allow you to view old versions +of datasets and changes (diffs) between them. + +This should be run once you've upgraded to CKAN 2.9. + +This script is not part of the main migrations because it takes a long time to +run, and you don't want it to delay a site going live again after an upgrade. +In the period between upgrading CKAN and this script completes, the Activity +Stream's view of old versions of datasets and diffs between them will be +incomplete - it won't show resources, extras or tags. + +This script is idempotent - there is no harm in running this multiple times, or +stopping and restarting it. + +We won't delete the revision tables in the database yet, since we haven't +converted the group, package_relationship to activity objects yet. + +(In a future version of CKAN we will remove the 'package_revision' table from +the codebase. We'll need a step in the main migration which checks that +migrate_package_activity.py has been done, before it removes the +package_revision table.) +''' + +# This code is not part of the main CKAN CLI because it is a one-off migration, +# whereas the main CLI is a list of tools for more frequent use. + +from __future__ import print_function +import argparse +import sys +from collections import defaultdict +from six.moves import input +from six import text_type + +# not importing anything from ckan until after the arg parsing, to fail on bad +# args quickly. + +_context = None + + +def get_context(): + from ckan import model + import ckan.logic as logic + global _context + if not _context: + user = logic.get_action(u'get_site_user')( + {u'model': model, u'ignore_auth': True}, {}) + _context = {u'model': model, u'session': model.Session, + u'user': user[u'name']} + return _context + + +def num_unmigrated(engine): + num_unmigrated = engine.execute(''' + SELECT count(*) FROM activity a JOIN package p ON a.object_id=p.id + WHERE a.activity_type IN ('new package', 'changed package') + AND a.data NOT LIKE '%%{"actor"%%' + AND p.private = false; + ''').fetchone()[0] + return num_unmigrated + + +def num_activities_migratable(): + from ckan import model + num_activities = model.Session.execute(u''' + SELECT count(*) FROM activity a JOIN package p ON a.object_id=p.id + WHERE a.activity_type IN ('new package', 'changed package') + AND p.private = false; + ''').fetchall()[0][0] + return num_activities + + +def migrate_all_datasets(): + import ckan.logic as logic + dataset_names = logic.get_action(u'package_list')(get_context(), {}) + num_datasets = len(dataset_names) + errors = defaultdict(int) + with PackageDictizeMonkeyPatch(): + for i, dataset_name in enumerate(dataset_names): + print(u'\n{}/{} dataset: {}' + .format(i + 1, num_datasets, dataset_name)) + migrate_dataset(dataset_name, errors) + print(u'Migrated:') + print(u' {} datasets'.format(len(dataset_names))) + num_activities = num_activities_migratable() + print(u' with {} activities'.format(num_activities)) + print_errors(errors) + + +class PackageDictizeMonkeyPatch(object): + '''Patches package_dictize to add back in the revision functionality. This + allows you to specify context['revision_id'] and see the old revisions of + a package. + + This works as a context object. We could have used mock.patch and saved a + couple of lines here, but we'd have had to add mock to requirements.txt. + ''' + def __enter__(self): + import ckan.lib.dictization.model_dictize as model_dictize + try: + import ckan.migration.revision_legacy_code as revision_legacy_code + except ImportError: + # convenient to look for it in the current directory if you just + # download these files because you are upgrading an older ckan + import revision_legacy_code + self.existing_function = model_dictize.package_dictize + model_dictize.package_dictize = \ + revision_legacy_code.package_dictize_with_revisions + + def __exit__(self, exc_type, exc_val, exc_tb): + import ckan.lib.dictization.model_dictize as model_dictize + model_dictize.package_dictize = self.existing_function + + +def migrate_dataset(dataset_name, errors): + ''' + NB this function should be run in a `with PackageDictizeMonkeyPatch():` + ''' + + import ckan.logic as logic + from ckan import model + + # 'hidden' activity is that by site_user, such as harvests, which are + # not shown in the activity stream because they can be too numerous. + # However these do have Activity objects, and if a hidden Activity is + # followed be a non-hidden one and you look at the changes of that + # non-hidden Activity, then it does a diff with the hidden one (rather than + # the most recent non-hidden one), so it is important to store the + # package_dict in hidden Activity objects. + package_activity_stream = logic.get_action(u'package_activity_list')( + get_context(), {u'id': dataset_name, u'include_hidden_activity': True}) + num_activities = len(package_activity_stream) + if not num_activities: + print(u' No activities') + + # Iterate over this package's existing activity stream objects + for i, activity in enumerate(reversed(package_activity_stream)): + # e.g. activity = + # {'activity_type': u'changed package', + # 'id': u'62107f87-7de0-4d17-9c30-90cbffc1b296', + # 'object_id': u'7c6314f5-c70b-4911-8519-58dc39a8e340', + # 'revision_id': u'c3e8670a-f661-40f4-9423-b011c6a3a11d', + # 'timestamp': '2018-04-20T16:11:45.363097', + # 'user_id': u'724273ac-a5dc-482e-add4-adaf1871f8cb'} + print(u' activity {}/{} {}'.format( + i + 1, num_activities, activity[u'timestamp'])) + + # we need activity.data and using the ORM is the fastest + activity_obj = model.Session.query(model.Activity).get(activity[u'id']) + if u'resources' in activity_obj.data.get(u'package', {}): + print(u' activity has full dataset already recorded' + ' - no action') + continue + + # get the dataset as it was at this revision: + # call package_show just as we do in package.py:activity_stream_item(), + # only with a revision_id (to get it as it was then) + context = dict( + get_context(), + for_view=False, + revision_id=activity[u'revision_id'], + use_cache=False, # avoid the cache (which would give us the + # latest revision) + ) + try: + dataset = logic.get_action(u'package_show')( + context, + {u'id': activity[u'object_id'], u'include_tracking': False}) + except Exception as exc: + if isinstance(exc, logic.NotFound): + error_msg = u'Revision missing' + else: + error_msg = text_type(exc) + print(u' Error: {}! Skipping this version ' + '(revision_id={})' + .format(error_msg, activity[u'revision_id'])) + errors[error_msg] += 1 + # We shouldn't leave the activity.data['package'] with missing + # resources, extras & tags, which could cause the package_read + # template to raise an exception, when user clicks "View this + # version". Instead we pare it down to use a title, and forgo + # viewing it. + try: + dataset = {u'title': activity_obj.data['package']['title']} + except KeyError: + # unlikely the package is not recorded in the activity, but + # not impossible + dataset = {u'title': u'unknown'} + + # get rid of revision_timestamp, which wouldn't be there if saved by + # during activity_stream_item() - something to do with not specifying + # revision_id. + if u'revision_timestamp' in (dataset.get(u'organization') or {}): + del dataset[u'organization'][u'revision_timestamp'] + for res in dataset.get(u'resources', []): + if u'revision_timestamp' in res: + del res[u'revision_timestamp'] + + actor = model.Session.query(model.User).get(activity[u'user_id']) + actor_name = actor.name if actor else activity[u'user_id'] + + # add the data to the Activity, just as we do in activity_stream_item() + data = { + u'package': dataset, + u'actor': actor_name, + } + activity_obj.data = data + # print ' {} dataset {}'.format(actor_name, repr(dataset)) + if model.Session.dirty: + model.Session.commit() + print(u' saved') + print(u' This package\'s {} activities are migrated'.format( + len(package_activity_stream))) + + +def wipe_activity_detail(delete_activity_detail): + from ckan import model + activity_detail_has_rows = \ + bool(model.Session.execute( + u'SELECT count(*) ' + 'FROM (SELECT * FROM "activity_detail" LIMIT 1) as t;') + .fetchall()[0][0]) + if not activity_detail_has_rows: + print(u'\nactivity_detail table is aleady emptied') + return + print( + u'\nNow the migration is done, the history of datasets is now stored\n' + 'in the activity table. As a result, the contents of the\n' + 'activity_detail table will no longer be used after CKAN 2.8.x, and\n' + 'you can delete it to save space (this is safely done before or\n' + 'after the CKAN upgrade).' + ) + if delete_activity_detail is None: + delete_activity_detail = \ + input(u'Delete activity_detail table content? (y/n):') + if delete_activity_detail.lower()[:1] != u'y': + return + from ckan import model + model.Session.execute(u'DELETE FROM "activity_detail";') + model.Session.commit() + print(u'activity_detail deleted') + + +def print_errors(errors): + if errors: + print(u'Error summary:') + for error_msg, count in errors.items(): + print(u' {} {}'.format(count, error_msg)) + print(u''' +These errors are unusual - maybe a dataset was deleted, purged and then +recreated, or the revisions corrupted for some reason. These activity items now +don't have a package_dict recorded against them, which means that when a user +clicks "View this version" or "Changes" in the Activity Stream for it, it will +be missing. Hopefully that\'s acceptable enough to just ignore, because these +errors are really hard to fix. + ''') + + +if __name__ == u'__main__': + parser = argparse.ArgumentParser(usage=__doc__) + parser.add_argument(u'-c', u'--config', help=u'CKAN config file (.ini)') + parser.add_argument(u'--delete', choices=[u'yes', u'no'], + help=u'Delete activity detail') + parser.add_argument(u'--dataset', help=u'just migrate this particular ' + u'dataset - specify its name') + args = parser.parse_args() + assert args.config, u'You must supply a --config' + try: + from ckan.lib.cli import load_config + except ImportError: + # for CKAN 2.6 and earlier + def load_config(config): + from ckan.lib.cli import CkanCommand + cmd = CkanCommand(name=None) + + class Options(object): + pass + cmd.options = Options() + cmd.options.config = config + cmd._load_config() + return + + print(u'Loading config') + load_config(args.config) + if not args.dataset: + migrate_all_datasets() + wipe_activity_detail(delete_activity_detail=args.delete) + else: + errors = defaultdict(int) + migrate_dataset(args.dataset, errors) + print_errors(errors) diff --git a/ckan/migration/revision_legacy_code.py b/ckan/migration/revision_legacy_code.py new file mode 100644 index 00000000000..47dc4c0fc24 --- /dev/null +++ b/ckan/migration/revision_legacy_code.py @@ -0,0 +1,493 @@ +# encoding: utf-8 + +import uuid +import datetime + +from sqlalchemy.sql import select +from sqlalchemy import and_, inspect +import sqlalchemy.orm.properties +from sqlalchemy.orm import class_mapper +from sqlalchemy.orm import relation +from vdm.sqlalchemy import add_fake_relation + +import ckan.logic as logic +import ckan.lib.dictization as d +from ckan.lib.dictization.model_dictize import ( + _execute, resource_list_dictize, extras_list_dictize, group_list_dictize) +from ckan import model + + +# This is based on ckan.lib.dictization.model_dictize:package_dictize +# BUT you can ask for a old revision to the package by specifying 'revision_id' +# in the context +def package_dictize_with_revisions(pkg, context): + ''' + Given a Package object, returns an equivalent dictionary. + + Normally this is the most recent version, but you can provide revision_id + or revision_date in the context and it will filter to an earlier time. + + May raise NotFound if: + * the specified revision_id doesn't exist + * the specified revision_date was before the package was created + ''' + model = context['model'] + is_latest_revision = not(context.get(u'revision_id') or + context.get(u'revision_date')) + execute = _execute if is_latest_revision else _execute_with_revision + # package + if is_latest_revision: + if isinstance(pkg, revision_model.PackageRevision): + pkg = model.Package.get(pkg.id) + result = pkg + else: + package_rev = revision_model.package_revision_table + q = select([package_rev]).where(package_rev.c.id == pkg.id) + result = execute(q, package_rev, context).first() + if not result: + raise logic.NotFound + result_dict = d.table_dictize(result, context) + # strip whitespace from title + if result_dict.get(u'title'): + result_dict['title'] = result_dict['title'].strip() + + # resources + if is_latest_revision: + res = model.resource_table + else: + res = revision_model.resource_revision_table + q = select([res]).where(res.c.package_id == pkg.id) + result = execute(q, res, context) + result_dict["resources"] = resource_list_dictize(result, context) + result_dict['num_resources'] = len(result_dict.get(u'resources', [])) + + # tags + tag = model.tag_table + if is_latest_revision: + pkg_tag = model.package_tag_table + else: + pkg_tag = revision_model.package_tag_revision_table + q = select([tag, pkg_tag.c.state], + from_obj=pkg_tag.join(tag, tag.c.id == pkg_tag.c.tag_id) + ).where(pkg_tag.c.package_id == pkg.id) + result = execute(q, pkg_tag, context) + result_dict["tags"] = d.obj_list_dictize(result, context, + lambda x: x["name"]) + result_dict['num_tags'] = len(result_dict.get(u'tags', [])) + + # Add display_names to tags. At first a tag's display_name is just the + # same as its name, but the display_name might get changed later (e.g. + # translated into another language by the multilingual extension). + for tag in result_dict['tags']: + assert u'display_name' not in tag + tag['display_name'] = tag['name'] + + # extras + if is_latest_revision: + extra = model.package_extra_table + else: + extra = revision_model.extra_revision_table + q = select([extra]).where(extra.c.package_id == pkg.id) + result = execute(q, extra, context) + result_dict["extras"] = extras_list_dictize(result, context) + + # groups + if is_latest_revision: + member = model.member_table + else: + member = revision_model.member_revision_table + group = model.group_table + q = select([group, member.c.capacity], + from_obj=member.join(group, group.c.id == member.c.group_id) + ).where(member.c.table_id == pkg.id)\ + .where(member.c.state == u'active') \ + .where(group.c.is_organization == False) # noqa + result = execute(q, member, context) + context['with_capacity'] = False + # no package counts as cannot fetch from search index at the same + # time as indexing to it. + # tags, extras and sub-groups are not included for speed + result_dict["groups"] = group_list_dictize(result, context, + with_package_counts=False) + + # owning organization + if is_latest_revision: + group = model.group_table + else: + group = revision_model.group_revision_table + q = select([group] + ).where(group.c.id == result_dict['owner_org']) \ + .where(group.c.state == u'active') + result = execute(q, group, context) + organizations = d.obj_list_dictize(result, context) + if organizations: + result_dict["organization"] = organizations[0] + else: + result_dict["organization"] = None + + # relations + if is_latest_revision: + rel = model.package_relationship_table + else: + rel = revision_model \ + .package_relationship_revision_table + q = select([rel]).where(rel.c.subject_package_id == pkg.id) + result = execute(q, rel, context) + result_dict["relationships_as_subject"] = \ + d.obj_list_dictize(result, context) + q = select([rel]).where(rel.c.object_package_id == pkg.id) + result = execute(q, rel, context) + result_dict["relationships_as_object"] = \ + d.obj_list_dictize(result, context) + + # Extra properties from the domain object + # We need an actual Package object for this, not a PackageRevision + # if isinstance(pkg, model.PackageRevision): + # pkg = model.Package.get(pkg.id) + + # isopen + result_dict['isopen'] = pkg.isopen if isinstance(pkg.isopen, bool) \ + else pkg.isopen() + + # type + # if null assign the default value to make searching easier + result_dict['type'] = pkg.type or u'dataset' + + # license + if pkg.license and pkg.license.url: + result_dict['license_url'] = pkg.license.url + result_dict['license_title'] = pkg.license.title.split(u'::')[-1] + elif pkg.license: + result_dict['license_title'] = pkg.license.title + else: + result_dict['license_title'] = pkg.license_id + + # creation and modification date + if is_latest_revision: + result_dict['metadata_modified'] = pkg.metadata_modified.isoformat() + # (If not is_latest_revision, don't use pkg which is the latest version. + # Instead, use the dates already in result_dict that came from the dictized + # PackageRevision) + result_dict['metadata_created'] = pkg.metadata_created.isoformat() + + return result_dict + + +def _execute_with_revision(q, rev_table, context): + ''' + Takes an SqlAlchemy query (q) that is (at its base) a Select on an object + revision table (rev_table), and you provide revision_id or revision_date in + the context and it will filter the object revision(s) to an earlier time. + + Raises NotFound if context['revision_id'] is provided, but the revision + ID does not exist. + + Returns [] if there are no results. + + ''' + model = context['model'] + session = model.Session + revision_id = context.get(u'revision_id') + revision_date = context.get(u'revision_date') + + if revision_id: + revision = session.query(revision_model.Revision) \ + .filter_by(id=revision_id).first() + if not revision: + raise logic.NotFound + revision_date = revision.timestamp + + q = q.where(rev_table.c.revision_timestamp <= revision_date) + q = q.where(rev_table.c.expired_timestamp > revision_date) + + return session.execute(q) + + +# Copied from vdm BUT without '.continuity' mapped to the base object +def create_object_version(mapper_fn, base_object, rev_table): + '''Create the Version Domain Object corresponding to base_object. + + E.g. if Package is our original object we should do:: + + # name of Version Domain Object class + PackageVersion = create_object_version(..., Package, ...) + + NB: This must obviously be called after mapping has happened to + base_object. + ''' + # TODO: can we always assume all versioned objects are stateful? + # If not need to do an explicit check + class MyClass(object): + def __init__(self, **kw): + for k, v in kw.iteritems(): + setattr(self, k, v) + + name = base_object.__name__ + u'Revision' + MyClass.__name__ = str(name) + MyClass.__continuity_class__ = base_object + + # Must add this so base object can retrieve revisions ... + base_object.__revision_class__ = MyClass + + ourmapper = mapper_fn( + MyClass, rev_table, + # NB: call it all_revisions_... rather than just revisions_... as it + # will yield all revisions not just those less than the current + # revision + + # --------------------- + # Deviate from VDM here + # + # properties={ + # 'continuity':relation(base_object, + # backref=backref('all_revisions_unordered', + # cascade='all, delete, delete-orphan'), + # order_by=rev_table.c.revision_id.desc() + # ), + # }, + # order_by=[rev_table.c.continuity_id, rev_table.c.revision_id.desc()] + # --------------------- + ) + base_mapper = class_mapper(base_object) + # add in 'relationship' stuff from continuity onto revisioned obj + # 3 types of relationship + # 1. scalar (i.e. simple fk) + # 2. list (has many) (simple fk the other way) + # 3. list (m2m) (join table) + # + # Also need to check whether related object is revisioned + # + # If related object is revisioned then can do all of these + # If not revisioned can only support simple relation (first case -- why?) + for prop in base_mapper.iterate_properties: + try: + is_relation = prop.__class__ == \ + sqlalchemy.orm.properties.PropertyLoader + except AttributeError: + # SQLAlchemy 0.9 + is_relation = prop.__class__ == \ + sqlalchemy.orm.properties.RelationshipProperty + + if is_relation: + # in sqlachemy 0.4.2 + # prop_remote_obj = prop.select_mapper.class_ + # in 0.4.5 + prop_remote_obj = prop.argument + remote_obj_is_revisioned = \ + getattr(prop_remote_obj, u'__revisioned__', False) + # this is crude, probably need something better + is_many = (prop.secondary is not None or prop.uselist) + if remote_obj_is_revisioned: + propname = prop.key + add_fake_relation(MyClass, propname, is_many=is_many) + elif not is_many: + ourmapper.add_property(prop.key, relation(prop_remote_obj)) + else: + # TODO: actually deal with this + # raise a warning of some kind + msg = \ + u'Skipping adding property %s to revisioned object' % prop + + return MyClass + + +# Tests use this to manually create revisions, that look just like how +# CKAN<=2.8 used to create automatically. +def make_package_revision(package): + '''Manually create a revision for a package and its related objects + ''' + # So far only PackageExtra needs manually creating - the rest still happen + # automatically + instances = [] + extras = model.Session.query(model.PackageExtra) \ + .filter_by(package_id=package.id) \ + .all() + instances.extend(extras) + make_revision(instances) + + +# Tests use this to manually create revisions, that look just like how +# CKAN<=2.8 used to create automatically. +def make_revision(instances): + '''Manually create a revision. + + Copies a new/changed row from a table (e.g. Package) into its + corresponding revision table (e.g. PackageRevision) and makes an entry + in the Revision table. + ''' + # model.repo.new_revision() was called in the model code, which is: + # vdm.sqlalchemy.tools.Repository.new_revision() which did this: + Revision = RevisionTableMappings.instance().Revision + revision = Revision() + model.Session.add(revision) + # new_revision then calls: + # SQLAlchemySession.set_revision(self.session, rev), which is: + # vdm.sqlalchemy.base.SQLAlchemySession.set_revision() which did this: + revision.id = str(uuid.uuid4()) + model.Session.add(revision) + model.Session.flush() + + # In CKAN<=2.8 the revisioned tables (e.g. Package) had a mapper + # extension: vdm.sqlalchemy.Revisioner(package_revision_table) + # that triggered on table changes and records a copy in the + # corresponding revision table (e.g. PackageRevision). + + # In Revisioner.before_insert() it does this: + for instance in instances: + is_changed = True # fake: check_real_change(instance) + if is_changed: + # set_revision(instance) + # which does this: + instance.revision = revision + instance.revision_id = revision.id + # Unfortunately modifying the Package (or whatever the instances are) + # will create another Activity object when we save the session, so + # delete that + existing_latest_activity = model.Session.query(model.Activity) \ + .order_by(model.Activity.timestamp.desc()).first() + model.Session.commit() + new_latest_activity = model.Session.query(model.Activity) \ + .order_by(model.Activity.timestamp.desc()).first() + if new_latest_activity != existing_latest_activity: + new_latest_activity.delete() + model.Session.commit() + + # In Revision.after_update() or after_insert() it does this: + # self.make_revision(instance, mapper, connection) + # which is vdm.sqlalchemy.base.Revisioner.make_revision() + # which copies the Package to make a new PackageRevision + for instance in instances: + colvalues = {} + mapper = inspect(type(instance)) + table = mapper.tables[0] + for key in table.c.keys(): + val = getattr(instance, key) + colvalues[key] = val + assert instance.revision.id + colvalues['revision_id'] = instance.revision.id + colvalues['continuity_id'] = instance.id + + # Allow for multiple SQLAlchemy flushes/commits per VDM revision + revision_table = \ + RevisionTableMappings.instance() \ + .revision_table_mapping[type(instance)] + ins = revision_table.insert().values(colvalues) + model.Session.execute(ins) + + # the related Activity would get the revision_id added to it too. + # Here we simply assume it's the latest activity. + activity = model.Session.query(model.Activity) \ + .order_by(model.Activity.timestamp.desc()) \ + .first() + activity.revision_id = revision.id + model.Session.flush() + + # In CKAN<=2.8 the session extension CkanSessionExtension had some + # extra code in before_commit() which wrote `revision_timestamp` and + # `expired_timestamp` values in the revision tables + # (e.g. package_revision) so that is added here: + for instance in instances: + if not hasattr(instance, u'__revision_class__'): + continue + revision_cls = instance.__revision_class__ + revision_table = \ + RevisionTableMappings.instance() \ + .revision_table_mapping[type(instance)] + # when a normal active transaction happens + + # this is an sql statement as we do not want it in object cache + model.Session.execute( + revision_table.update().where( + and_(revision_table.c.id == instance.id, + revision_table.c.current == u'1') + ).values(current=u'0') + ) + + q = model.Session.query(revision_cls) + q = q.filter_by(expired_timestamp=datetime.datetime(9999, 12, 31), + id=instance.id) + results = q.all() + for rev_obj in results: + values = {} + if rev_obj.revision_id == revision.id: + values['revision_timestamp'] = revision.timestamp + else: + values['expired_timestamp'] = revision.timestamp + model.Session.execute( + revision_table.update().where( + and_(revision_table.c.id == rev_obj.id, + revision_table.c.revision_id == rev_obj.revision_id) + ).values(**values) + ) + + +# Revision tables (singleton) +class RevisionTableMappings(object): + _instance = None + + @classmethod + def instance(cls): + if not cls._instance: + cls._instance = cls() + return cls._instance + + def __init__(self): + # This uses the strangler pattern to gradually move the revision model + # out of ckan/model and into this file. + # We start with references to revision model in ckan/model/ and then + # gradually move the definitions here + self.revision_table = model.revision_table + + self.Revision = model.Revision + + self.package_revision_table = model.package_revision_table + self.PackageRevision = model.PackageRevision + + self.resource_revision_table = model.resource_revision_table + self.ResourceRevision = model.ResourceRevision + + self.extra_revision_table = model.extra_revision_table + self.PackageExtraRevision = create_object_version( + model.meta.mapper, model.PackageExtra, + self.extra_revision_table) + + self.package_tag_revision_table = model.package_tag_revision_table + self.PackageTagRevision = model.PackageTagRevision + + self.member_revision_table = model.member_revision_table + self.MemberRevision = model.MemberRevision + + self.group_revision_table = model.group_revision_table + self.GroupRevision = model.GroupRevision + + self.group_extra_revision_table = model.group_extra_revision_table + self.GroupExtraRevision = create_object_version( + model.meta.mapper, model.GroupExtra, + self.group_extra_revision_table) + + self.package_relationship_revision_table = \ + model.package_relationship_revision_table + self.PackageRelationshipRevision = model.PackageRelationshipRevision + + self.system_info_revision_table = model.system_info_revision_table + self.SystemInfoRevision = model.SystemInfoRevision + + self.revision_table_mapping = { + model.Package: self.package_revision_table, + model.Resource: self.resource_revision_table, + model.PackageExtra: self.extra_revision_table, + model.PackageTag: self.package_tag_revision_table, + model.Member: self.member_revision_table, + model.Group: self.group_revision_table, + } + + +# It's easiest if this code works on all versions of CKAN. After CKAN 2.8 the +# revision model is separate from the main model. +try: + model.PackageExtraRevision + # CKAN<=2.8 + revision_model = model +except AttributeError: + # CKAN>2.8 + revision_model = RevisionTableMappings.instance() diff --git a/ckan/migration/versions/089_package_activity_migration_check.py b/ckan/migration/versions/089_package_activity_migration_check.py new file mode 100644 index 00000000000..aed0a797e61 --- /dev/null +++ b/ckan/migration/versions/089_package_activity_migration_check.py @@ -0,0 +1,38 @@ +# encoding: utf-8 + +import sys + +from ckan.migration.migrate_package_activity import num_unmigrated + + +def upgrade(migrate_engine): + num_unmigrated_dataset_activities = num_unmigrated(migrate_engine) + if num_unmigrated_dataset_activities: + print(''' + NOTE: + You have {num_unmigrated} unmigrated package activities. + + Once your CKAN upgrade is complete and CKAN server is running again, you + should run the package activity migration, so that the Activity Stream can + display the detailed history of datasets: + + python migrate_package_activity.py -c /etc/ckan/production.ini + + Once you've done that, the detailed history is visible in Activity Stream + to *admins only*. However you are encouraged to make it available to the + public, by setting this in production.ini: + + ckan.auth.public_activity_stream_detail = true + + More information about all of this is here: + https://github.com/ckan/ckan/wiki/Migrate-package-activity + '''.format(num_unmigrated=num_unmigrated_dataset_activities)) + else: + # there are no unmigrated package activities + are_any_datasets = bool( + migrate_engine.execute(u'SELECT id FROM PACKAGE LIMIT 1').rowcount) + # no need to tell the user if there are no datasets - this could just + # be a fresh CKAN install + if are_any_datasets: + print(u'You have no unmigrated package activities - you do not ' + 'need to run migrate_package_activity.py.') diff --git a/ckan/model/__init__.py b/ckan/model/__init__.py index c00755c0b65..4d3aee7480f 100644 --- a/ckan/model/__init__.py +++ b/ckan/model/__init__.py @@ -60,10 +60,12 @@ from group_extra import ( GroupExtra, group_extra_table, + group_extra_revision_table, ) from package_extra import ( PackageExtra, package_extra_table, + extra_revision_table, ) from resource import ( Resource, @@ -88,6 +90,7 @@ ) from package_relationship import ( PackageRelationship, + PackageRelationshipRevision, package_relationship_table, package_relationship_revision_table, ) diff --git a/ckan/model/activity.py b/ckan/model/activity.py index c93965105fc..d311665c618 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -61,10 +61,19 @@ def __init__( else: self.data = data + @classmethod + def get(cls, id): + '''Returns an Activity object referenced by its id.''' + if not id: + return None + + return meta.Session.query(cls).get(id) + meta.mapper(Activity, activity_table) +# deprecated class ActivityDetail(domain_object.DomainObject): def __init__( @@ -105,7 +114,7 @@ def _activities_limit(q, limit, offset=None): def _activities_union_all(*qlist): ''' - Return union of two or more queries sorted by timestamp, + Return union of two or more activity queries sorted by timestamp, and remove duplicates ''' import ckan.model as model @@ -208,12 +217,13 @@ def _group_activity_query(group_id): ).outerjoin( model.Package, and_( - model.Package.id == model.Member.table_id, + or_(model.Package.id == model.Member.table_id, + model.Package.owner_org == group_id), model.Package.private == False, ) ).filter( - # We only care about activity either on the the group itself or on - # packages within that group. + # We only care about activity either on 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. diff --git a/ckan/model/package.py b/ckan/model/package.py index 5ba6854eeb3..56997589436 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -402,56 +402,6 @@ def latest_related_revision(self): objects.''' return self.all_related_revisions[0][0] - def diff(self, to_revision=None, from_revision=None): - '''Overrides the diff in vdm, so that related obj revisions are - diffed as well as PackageRevisions''' - from tag import PackageTag - from resource import Resource - from package_extra import PackageExtra - - results = {} # field_name:diffs - results.update(super(Package, self).diff(to_revision, from_revision)) - # Iterate over PackageTag, Resources etc. (NB PackageExtra is not - # revisioned any more, so the diff is incomplete) - for obj_class in [Resource, PackageTag]: - obj_rev_class = obj_class.__revision_class__ - # Query for object revisions related to this package - obj_rev_query = meta.Session.query(obj_rev_class).\ - filter_by(package_id=self.id).\ - join('revision').\ - order_by(core.Revision.timestamp.desc()) - # Columns to include in the diff - cols_to_diff = obj_class.revisioned_fields() - cols_to_diff.remove('id') - if obj_class is Resource: - cols_to_diff.remove('package_id') - # Particular object types are better known by an invariant field - if obj_class is PackageTag: - cols_to_diff.remove('tag_id') - elif obj_class is PackageExtra: - cols_to_diff.remove('key') - # Iterate over each object ID - # e.g. for PackageTag, iterate over Tag objects - related_obj_ids = set([related_obj.id for related_obj in obj_rev_query.all()]) - for related_obj_id in related_obj_ids: - q = obj_rev_query.filter(obj_rev_class.id==related_obj_id) - to_obj_rev, from_obj_rev = super(Package, self).\ - get_obj_revisions_to_diff( - q, to_revision, from_revision) - for col in cols_to_diff: - values = [getattr(obj_rev, col) if obj_rev else '' for obj_rev in (from_obj_rev, to_obj_rev)] - value_diff = self._differ(*values) - if value_diff: - if obj_class.__name__ == 'PackageTag': - display_id = to_obj_rev.tag.name - elif obj_class.__name__ == 'PackageExtra': - display_id = to_obj_rev.key - else: - display_id = related_obj_id[:4] - key = '%s-%s-%s' % (obj_class.__name__, display_id, col) - results[key] = value_diff - return results - @property @maintain.deprecated('`is_private` attriute of model.Package is ' + 'deprecated and should not be used. Use `private`') @@ -528,10 +478,17 @@ def activity_stream_item(self, activity_type, revision, user_id): activity_type = 'deleted' try: - d = {'package': dictization.table_dictize(self, - context={'model': ckan.model})} - return activity.Activity(user_id, self.id, revision.id, - "%s package" % activity_type, d) + # We save the entire rendered package dict so we can support + # viewing the past packages from the activity feed. + dictized_package = ckan.logic.get_action('package_show')({ + 'model': ckan.model, + 'session': ckan.model.Session, + 'for_view': False, # avoid ckanext-multilingual translating it + 'ignore_auth': True + }, { + 'id': self.id, + 'include_tracking': False + }) except ckan.logic.NotFound: # This happens if this package is being purged and therefore has no # current revision. @@ -539,20 +496,20 @@ def activity_stream_item(self, activity_type, revision, user_id): # is purged. return None - def activity_stream_detail(self, activity_id, activity_type): - import ckan.model - - # Handle 'deleted' objects. - # When the user marks a package as deleted this comes through here as - # a 'changed' package activity. We detect this and change it to a - # 'deleted' activity. - if activity_type == 'changed' and self.state == u'deleted': - activity_type = 'deleted' - - package_dict = dictization.table_dictize(self, - context={'model':ckan.model}) - return activity.ActivityDetail(activity_id, self.id, u"Package", activity_type, - {'package': package_dict }) + actor = meta.Session.query(ckan.model.User).get(user_id) + + return activity.Activity( + user_id, + self.id, + revision.id, + "%s package" % activity_type, + { + 'package': dictized_package, + # We keep the acting user name around so that actions can be + # properly displayed even if the user is deleted in the future. + 'actor': actor.name if actor else None + } + ) def set_rating(self, user_or_ip, rating): '''Record a user's rating of this package. diff --git a/ckan/model/package_extra.py b/ckan/model/package_extra.py index 6e1a46f8e94..fd9ee65ec79 100644 --- a/ckan/model/package_extra.py +++ b/ckan/model/package_extra.py @@ -38,20 +38,6 @@ class PackageExtra( def related_packages(self): return [self.package] - def activity_stream_detail(self, activity_id, activity_type): - import ckan.model as model - - # Handle 'deleted' extras. - # When the user marks an extra as deleted this comes through here as a - # 'changed' extra. We detect this and change it to a 'deleted' - # activity. - if activity_type == 'changed' and self.state == u'deleted': - activity_type = 'deleted' - - data_dict = ckan.lib.dictization.table_dictize(self, - context={'model': model}) - return activity.ActivityDetail(activity_id, self.id, u"PackageExtra", - activity_type, {'package_extra': data_dict}) meta.mapper(PackageExtra, package_extra_table, properties={ 'package': orm.relation(_package.Package, diff --git a/ckan/model/resource.py b/ckan/model/resource.py index 43d99dbbd39..3b591ef9ab7 100644 --- a/ckan/model/resource.py +++ b/ckan/model/resource.py @@ -158,23 +158,6 @@ def get_all_without_views(cls, formats=[]): def related_packages(self): return [self.package] - def activity_stream_detail(self, activity_id, activity_type): - import ckan.model as model - - # Handle 'deleted' resources. - # When the user marks a resource as deleted this comes through here as - # a 'changed' resource activity. We detect this and change it to a - # 'deleted' activity. - if activity_type == 'changed' and self.state == u'deleted': - activity_type = 'deleted' - - res_dict = ckan.lib.dictization.table_dictize(self, - context={'model': model}) - return activity.ActivityDetail(activity_id, self.id, u"Resource", - activity_type, - {'resource': res_dict}) - - ## Mappers diff --git a/ckan/model/tag.py b/ckan/model/tag.py index ab240cf0bd4..92f7bef8699 100644 --- a/ckan/model/tag.py +++ b/ckan/model/tag.py @@ -231,32 +231,6 @@ def __repr__(self): s = u'' % (self.package.name, self.tag.name) return s.encode('utf8') - def activity_stream_detail(self, activity_id, activity_type): - if activity_type == 'new': - # New PackageTag objects are recorded as 'added tag' activities. - activity_type = 'added' - elif activity_type == 'changed': - # Changed PackageTag objects are recorded as 'removed tag' - # activities. - # FIXME: This assumes that whenever a PackageTag is changed it's - # because its' state has been changed from 'active' to 'deleted'. - # Should do something more here to test whether that is in fact - # what has changed. - activity_type = 'removed' - else: - return None - - # Return an 'added tag' or 'removed tag' activity. - import ckan.model as model - c = {'model': model} - d = {'tag': ckan.lib.dictization.table_dictize(self.tag, c), - 'package': ckan.lib.dictization.table_dictize(self.package, c)} - return activity.ActivityDetail( - activity_id=activity_id, - object_id=self.id, - object_type='tag', - activity_type=activity_type, - data=d) @classmethod def by_name(self, package_name, tag_name, vocab_id_or_name=None, diff --git a/ckan/pastertemplates/template/+dot+travis.yml_tmpl b/ckan/pastertemplates/template/+dot+travis.yml_tmpl index a05c2992369..7d9dad1e2f6 100644 --- a/ckan/pastertemplates/template/+dot+travis.yml_tmpl +++ b/ckan/pastertemplates/template/+dot+travis.yml_tmpl @@ -1,14 +1,44 @@ language: python sudo: required + +# use an older trusty image, because the newer images cause build errors with +# psycopg2 that comes with CKAN<2.8: +#  "Error: could not determine PostgreSQL version from '10.1'" +# see https://github.com/travis-ci/travis-ci/issues/8897 +dist: trusty +group: deprecated-2017Q4 + +# matrix python: - - "2.7" + - 2.7 +env: + - CKANVERSION=master + - CKANVERSION=2.7 + - CKANVERSION=2.8 + +# tests services: - - postgresql - - redis-server + - postgresql + - redis-server install: - - bash bin/travis-build.bash - - pip install coveralls + - bash bin/travis-build.bash + - pip install coveralls script: sh bin/travis-run.sh after_success: - - coveralls + - coveralls +# additional jobs +matrix: + include: + - name: "Flake8 on Python 3.7" + dist: xenial # required for Python 3.7 + cache: pip + install: pip install flake8 + script: + - flake8 --version + - flake8 . --count --max-complexity=10 --max-line-length=127 --statistics --exclude ckan,{{ project }} + python: 3.7 + # overwrite matrix + env: + - FLAKE8=true + - CKANVERSION=master diff --git a/ckan/pastertemplates/template/bin/travis-build.bash_tmpl b/ckan/pastertemplates/template/bin/travis-build.bash_tmpl index ae36e7313ad..55fa2dbc896 100755 --- a/ckan/pastertemplates/template/bin/travis-build.bash_tmpl +++ b/ckan/pastertemplates/template/bin/travis-build.bash_tmpl @@ -10,22 +10,44 @@ sudo apt-get install solr-jetty echo "Installing CKAN and its Python dependencies..." git clone https://github.com/ckan/ckan cd ckan -export latest_ckan_release_branch=`git branch --all | grep remotes/origin/release-v | sort -r | sed 's/remotes\/origin\///g' | head -n 1` -echo "CKAN branch: $latest_ckan_release_branch" -git checkout $latest_ckan_release_branch +if [ $CKANVERSION == 'master' ] +then + echo "CKAN version: master" +else + CKAN_TAG=$(git tag | grep ^ckan-$CKANVERSION | sort --version-sort | tail -n 1) + git checkout $CKAN_TAG + echo "CKAN version: ${CKAN_TAG#ckan-}" +fi + +# install the recommended version of setuptools +if [ -f requirement-setuptools.txt ] +then + echo "Updating setuptools..." + pip install -r requirement-setuptools.txt +fi + +if [ $CKANVERSION == '2.7' ] +then + echo "Installing setuptools" + pip install setuptools==39.0.1 +fi + python setup.py develop -pip install -r requirements.txt --allow-all-external -pip install -r dev-requirements.txt --allow-all-external +pip install -r requirements.txt +pip install -r dev-requirements.txt cd - echo "Creating the PostgreSQL user and database..." sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';" sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;' -echo "SOLR config..." +echo "Setting up Solr..." # Solr is multicore for tests on ckan master, but it's easier to run tests on # Travis single-core. See https://github.com/ckan/ckan/issues/2972 sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8983\/solr/' ckan/test-core.ini +printf "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty +sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml +sudo service jetty restart echo "Initialising the database..." cd ckan diff --git a/ckan/pastertemplates/template/bin/travis-run.sh_tmpl b/ckan/pastertemplates/template/bin/travis-run.sh_tmpl index b737690eeb5..56e9548c07d 100755 --- a/ckan/pastertemplates/template/bin/travis-run.sh_tmpl +++ b/ckan/pastertemplates/template/bin/travis-run.sh_tmpl @@ -1,8 +1,9 @@ #!/bin/sh -e +set -ex -echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty -sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml -sudo service jetty restart +flake8 --version +# stop the build if there are Python syntax errors or undefined names +flake8 . --count --select=E901,E999,F821,F822,F823 --show-source --statistics --exclude ckan,{{ project }} nosetests --ckan \ --nologcapture \ @@ -13,3 +14,5 @@ nosetests --ckan \ --cover-erase \ --cover-tests +# strict linting +flake8 . --count --max-complexity=10 --max-line-length=127 --statistics --exclude ckan,{{ project }} diff --git a/ckan/pastertemplates/template/dev-requirements.txt_tmpl b/ckan/pastertemplates/template/dev-requirements.txt_tmpl index e69de29bb2d..66cb8c06c14 100644 --- a/ckan/pastertemplates/template/dev-requirements.txt_tmpl +++ b/ckan/pastertemplates/template/dev-requirements.txt_tmpl @@ -0,0 +1 @@ +flake8 # for the travis build diff --git a/ckan/plugins/interfaces.py b/ckan/plugins/interfaces.py index 6fc92435aec..b73ae48d20b 100644 --- a/ckan/plugins/interfaces.py +++ b/ckan/plugins/interfaces.py @@ -1243,12 +1243,9 @@ def search_template(self): ''' def history_template(self): - u'''Return the path to the template for the dataset history page. - - The path should be relative to the plugin's templates dir, e.g. - ``'package/history.html'``. - - :rtype: string + u''' + .. warning:: This template is removed. The function exists for + compatibility. It now returns None. ''' diff --git a/ckan/plugins/toolkit.py b/ckan/plugins/toolkit.py index da2fbcd8cca..ce107f0f20f 100644 --- a/ckan/plugins/toolkit.py +++ b/ckan/plugins/toolkit.py @@ -475,7 +475,7 @@ def _get_endpoint(cls): # there are some routes('hello_world') that are not using blueprint # For such case, let's assume that view function is a controller # itself and action is None. - if len(endpoint) is 1: + if len(endpoint) == 1: return endpoint + (None,) return endpoint diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index fd966a245e9..27f04856261 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -8569,6 +8569,9 @@ fieldset[disabled] .control-custom.disabled .checkbox.btn.focus { .resource-view { margin-top: 20px; } +#activity-archive-notice { + clear: both; +} .search-form { margin-bottom: 20px; padding-bottom: 25px; diff --git a/ckan/public/base/less/dataset.less b/ckan/public/base/less/dataset.less index a3da83d2d8f..0e4390f92e4 100644 --- a/ckan/public/base/less/dataset.less +++ b/ckan/public/base/less/dataset.less @@ -338,3 +338,7 @@ .resource-view { margin-top: 20px; } + +#activity-archive-notice { + clear: both; +} diff --git a/ckan/templates-bs2/group/history.html b/ckan/templates-bs2/group/history.html deleted file mode 100644 index 3481770ef85..00000000000 --- a/ckan/templates-bs2/group/history.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "group/read_base.html" %} - -{% block subtitle %}{{ _('History') }} {{ g.template_title_delimiter }} {{ group_dict.display_name }}{% endblock %} - -{% block primary_content_inner %} -

{{ _('History') }}

- {% block group_history_revisions %} - {% snippet "group/snippets/history_revisions.html", group_dict=group_dict, group_revisions=group_revisions %} - {% endblock %} -{% endblock %} diff --git a/ckan/templates-bs2/group/snippets/history_revisions.html b/ckan/templates-bs2/group/snippets/history_revisions.html deleted file mode 100644 index babb24a121b..00000000000 --- a/ckan/templates-bs2/group/snippets/history_revisions.html +++ /dev/null @@ -1,12 +0,0 @@ -{% import 'macros/form.html' as form %} - -
- - {{ form.errors(error_summary) }} - - - {% snippet 'group/snippets/revisions_table.html', group_dict=group_dict, group_revisions=group_revisions %} - - - -
\ No newline at end of file diff --git a/ckan/templates-bs2/group/snippets/revisions_table.html b/ckan/templates-bs2/group/snippets/revisions_table.html deleted file mode 100644 index 5b7f2eeec66..00000000000 --- a/ckan/templates-bs2/group/snippets/revisions_table.html +++ /dev/null @@ -1,31 +0,0 @@ -{% import 'macros/form.html' as form %} - - - - - - - - - - - - - {% for rev in group_revisions %} - - - - - - - - {% endfor %} - -
{{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Log Message') }}
- {{ h.radio('selected1', rev.id, checked=(loop.first)) }} - {{ h.radio('selected2', rev.id, checked=(loop.last)) }} - - {% link_for rev.id | truncate(6), controller='revision', action='read', id=rev.id %} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }}{{ rev.message }}
\ No newline at end of file diff --git a/ckan/templates-bs2/package/base.html b/ckan/templates-bs2/package/base.html index 8d0ef0ce83f..8f90983739e 100644 --- a/ckan/templates-bs2/package/base.html +++ b/ckan/templates-bs2/package/base.html @@ -16,7 +16,7 @@ {% else %}
  • {% link_for _('Datasets'), named_route='dataset.search' %}
  • {% endif %} - {% link_for dataset|truncate(30), named_route='dataset.read', id=pkg.name %} + {% link_for dataset|truncate(30), named_route='dataset.read', id=pkg.id if is_activity_archive else pkg.name %} {% else %}
  • {% link_for _('Datasets'), named_route='dataset.search' %}
  • {{ _('Create Dataset') }}
  • diff --git a/ckan/templates-bs2/package/base_form_page.html b/ckan/templates-bs2/package/base_form_page.html index fe2b7d5e1d9..379f214dcfd 100644 --- a/ckan/templates-bs2/package/base_form_page.html +++ b/ckan/templates-bs2/package/base_form_page.html @@ -31,6 +31,6 @@

    {{ _('What are data {% block resources_module %} {# TODO: Pass in a list of previously created resources and the current package dict #} - {% snippet "package/snippets/resources.html", pkg={}, action='new_resource' %} + {% snippet "package/snippets/resources.html", pkg={}, action='new_resource', is_activity_archive=False %} {% endblock %} {% endblock %} diff --git a/ckan/templates-bs2/package/history.html b/ckan/templates-bs2/package/history.html deleted file mode 100644 index a4e81d4821b..00000000000 --- a/ckan/templates-bs2/package/history.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "package/read_base.html" %} - -{% block subtitle %}{{ _('History') }} {{ g.template_title_delimiter }} {{ h.dataset_display_name(c.pkg_dict) }}{% endblock %} - -{% block primary_content_inner %} -

    {{ _('History') }}

    - {% block package_history_revisions %} - {% snippet "package/snippets/history_revisions.html", pkg_dict=pkg, pkg_revisions=c.pkg_revisions %} - {% endblock %} -{% endblock %} diff --git a/ckan/templates-bs2/package/new_resource_not_draft.html b/ckan/templates-bs2/package/new_resource_not_draft.html index e2ef4cb2e24..261b71ea0b3 100644 --- a/ckan/templates-bs2/package/new_resource_not_draft.html +++ b/ckan/templates-bs2/package/new_resource_not_draft.html @@ -9,7 +9,7 @@ {% endblock %} {% block form %} - {% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, include_metadata=false, pkg_name=pkg_name, stage=stage, allow_upload=g.ofs_impl and logged_in, dataset_type=dataset_type %} + {% 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 %} {% block content_primary_nav %} diff --git a/ckan/templates-bs2/package/read.html b/ckan/templates-bs2/package/read.html index 8246e2cc6ba..32c3c8b7cfb 100644 --- a/ckan/templates-bs2/package/read.html +++ b/ckan/templates-bs2/package/read.html @@ -9,6 +9,16 @@ {{ _('Private') }} {% endif %} + {% block package_archive_notice %} + {% if is_activity_archive %} +
    + {% trans url=h.url_for('dataset.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. To see the + current version, click here. + {% endtrans %} +
    + {% endif %} + {% endblock %}

    {% block page_heading %} {{ h.dataset_display_name(pkg) }} @@ -32,7 +42,7 @@

    {% endblock %} {% block package_resources %} - {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} + {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources, is_activity_archive=is_activity_archive %} {% endblock %} {% block package_tags %} diff --git a/ckan/templates-bs2/package/read_base.html b/ckan/templates-bs2/package/read_base.html index 3d6b466dc36..368b7eba1a0 100644 --- a/ckan/templates-bs2/package/read_base.html +++ b/ckan/templates-bs2/package/read_base.html @@ -10,30 +10,17 @@ {% endblock -%} {% block content_action %} - {% if h.check_access('package_update', {'id':pkg.id }) %} - {% link_for _('Manage'), named_route='dataset.edit', id=pkg.name, class_='btn', icon='wrench' %} + {% if not is_activity_archive %} + {% if h.check_access('package_update', {'id':pkg.id }) %} + {% link_for _('Manage'), named_route='dataset.edit', id=pkg.name, class_='btn', icon='wrench' %} + {% endif %} {% endif %} {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.name) }} - {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.name) }} - {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.name) }} -{% endblock %} - -{% block primary_content_inner %} - {% block package_revision_info %} - {% if c.revision_date %} -
    -

    - {% set timestamp = h.render_datetime(c.revision_date, with_hours=True) %} - {% set url = h.url_for('dataset.read', id=pkg.name) %} - - {% trans timestamp=timestamp, url=url %}This is an old revision of this dataset, as edited at {{ timestamp }}. It may differ significantly from the current revision.{% endtrans %} -

    -
    - {% endif %} - {% endblock %} + {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.id if is_activity_archive else pkg.name) }} + {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.id if is_activity_archive else pkg.name) }} + {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name) }} {% endblock %} {% block secondary_content %} @@ -46,7 +33,7 @@ {% block package_organization %} {% if pkg.organization %} - {% set org = h.get_organization(pkg.organization.name) %} + {% set org = h.get_organization(pkg.organization.id) %} {% snippet "snippets/organization.html", organization=org, has_context_title=true %} {% endif %} {% endblock %} diff --git a/ckan/templates-bs2/package/resource_edit.html b/ckan/templates-bs2/package/resource_edit.html index 6acb7929467..cb482bdb19d 100644 --- a/ckan/templates-bs2/package/resource_edit.html +++ b/ckan/templates-bs2/package/resource_edit.html @@ -9,7 +9,6 @@ error_summary=error_summary, pkg_name=pkg.name, form_action=form_action, - allow_upload=g.ofs_impl and logged_in, resource_form_snippet=resource_form_snippet, dataset_type=dataset_type %} {% endblock %} diff --git a/ckan/templates-bs2/package/resource_read.html b/ckan/templates-bs2/package/resource_read.html index 3ab83c4cafb..be2ea14987b 100644 --- a/ckan/templates-bs2/package/resource_read.html +++ b/ckan/templates-bs2/package/resource_read.html @@ -27,7 +27,7 @@ {% block resource_actions %}
      {% block resource_actions_inner %} - {% if h.check_access('package_update', {'id':pkg.id }) %} + {% if h.check_access('package_update', {'id':pkg.id }) and not is_activity_archive %}
    • {% link_for _('Manage'), named_route='resource.edit', id=pkg.name, resource_id=res.id, class_='btn', icon='wrench' %}
    • {% endif %} {% if res.url and h.is_url(res.url) %} @@ -44,7 +44,8 @@ {{ _('Download') }} {% endif %} - {% block download_resource_button %} {%if res.datastore_active %} + {% block download_resource_button %} + {% if res.datastore_active %} @@ -68,7 +69,8 @@
    - {%endif%} {% endblock %} + {% endif %} + {% endblock %} {% endif %} {% endblock %} @@ -76,6 +78,16 @@ {% endblock %} {% block resource_content %} + {% block package_archive_notice %} + {% if is_activity_archive %} +
    + {% trans url=h.url_for('dataset.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. To see the + current version, click here. + {% endtrans %} +
    + {% endif %} + {% endblock %} {% block resource_read_title %}

    {{ h.resource_display_name(res) | truncate(50) }}

    {% endblock %} {% block resource_read_url %} {% if res.url and h.is_url(res.url) %} @@ -91,11 +103,12 @@ {% if not res.description and package.notes %}

    {{ _('From the dataset abstract') }}

    {{ h.markdown_extract(h.get_translated(package, 'notes')) }}
    -

    {% trans dataset=package.title, url=h.url_for('dataset.read', id=package['name']) %}Source: {{ dataset }}{% endtrans %} +

    {% trans dataset=package.title, url=h.url_for('dataset.read', id=package.id if is_activity_archive else package.name) %}Source: {{ dataset }}{% endtrans %} {% endif %} {% endblock %} + {% if not is_activity_archive %} {% block data_preview %} {% block resource_view %} {% block resource_view_nav %} @@ -156,6 +169,7 @@

    {{ _('From the dataset abstract') }}

    {% endblock %} {% endblock %} + {% endif %} {% endblock %} {% endblock %} @@ -211,7 +225,7 @@

    {{ _('Additional Information') }}

    {% block secondary_content %} {% block resources_list %} - {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id %} + {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, action='read', is_activity_archive=is_activity_archive %} {% endblock %} {% block resource_license %} diff --git a/ckan/templates-bs2/package/resources.html b/ckan/templates-bs2/package/resources.html index 8de6b1a2ec0..275b6a54551 100644 --- a/ckan/templates-bs2/package/resources.html +++ b/ckan/templates-bs2/package/resources.html @@ -13,7 +13,7 @@
      {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} {% for resource in pkg.resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit, is_activity_archive=False %} {% endfor %}
    {% else %} diff --git a/ckan/templates-bs2/package/snippets/history_revisions.html b/ckan/templates-bs2/package/snippets/history_revisions.html deleted file mode 100644 index f75aafc5d3f..00000000000 --- a/ckan/templates-bs2/package/snippets/history_revisions.html +++ /dev/null @@ -1,12 +0,0 @@ -{% import 'macros/form.html' as form %} - -
    - - {{ form.errors(error_summary) }} - - - {% snippet 'package/snippets/revisions_table.html', pkg_dict=pkg_dict, pkg_revisions=pkg_revisions %} - - - -
    \ No newline at end of file diff --git a/ckan/templates-bs2/package/snippets/info.html b/ckan/templates-bs2/package/snippets/info.html index 995482432a0..20678ffc6a2 100644 --- a/ckan/templates-bs2/package/snippets/info.html +++ b/ckan/templates-bs2/package/snippets/info.html @@ -1,5 +1,5 @@ {# -Displays a sidebard module with information for given package +Displays a sidebar module with information for given package pkg - The package dict that owns the resources. diff --git a/ckan/templates-bs2/package/snippets/resource_item.html b/ckan/templates-bs2/package/snippets/resource_item.html index 53265ff57e5..b702b753717 100644 --- a/ckan/templates-bs2/package/snippets/resource_item.html +++ b/ckan/templates-bs2/package/snippets/resource_item.html @@ -1,11 +1,25 @@ +{# + Renders a single resource with icons and view links. + + res - A resource dict to render + pkg - A package dict that the resource belongs to + can_edit - Whether the user is allowed to edit the resource + url_is_edit - Whether the link to the resource should be to editing it (set to False to make the link view the resource) + is_activity_archive - Whether this is an old version of the dataset (and therefore read-only) + + Example: + + {% snippet "package/snippets/resource_item.html", res=resource, pkg_dict=pkg_dict, can_edit=True, url_is_edit=False %} + +#} {% set url_action = 'resource.edit' if url_is_edit and can_edit else 'resource.read' %} -{% set url = h.url_for(url_action, id=pkg.name, resource_id=res.id) %} +{% set url = h.url_for(url_action, id=pkg.id if is_activity_archive else pkg.name, resource_id=res.id, **({'activity_id': request.args['activity_id']} if 'activity_id' in request.args else {})) %}
  • {% block resource_item_title %} {{ h.resource_display_name(res) | truncate(50) }}{{ h.get_translated(res, 'format') }} - {{ h.popular('views', res.tracking_summary.total, min=10) }} + {{ h.popular('views', res.tracking_summary.total, min=10) if res.tracking_summary }} {% endblock %} {% block resource_item_description %} diff --git a/ckan/templates-bs2/package/snippets/resources.html b/ckan/templates-bs2/package/snippets/resources.html index 8c55f8ee4c4..09823622435 100644 --- a/ckan/templates-bs2/package/snippets/resources.html +++ b/ckan/templates-bs2/package/snippets/resources.html @@ -5,10 +5,11 @@ pkg - The package dict that owns the resources. active - The id of the currently displayed resource. action - The resource action to use (default: 'read', meaning route 'resource.read'). +is_activity_archive - Whether this is an old version of the resource (and therefore read-only) Example: - {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id %} + {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, is_activity_archive=False %} #} {% set resources = pkg.resources or [] %} @@ -23,7 +24,7 @@

    {{ _("Resources") }} {% for resource in resources %} {% endfor %} diff --git a/ckan/templates-bs2/package/snippets/resources_list.html b/ckan/templates-bs2/package/snippets/resources_list.html index e7b8e8ed9df..8c33546d6ca 100644 --- a/ckan/templates-bs2/package/snippets/resources_list.html +++ b/ckan/templates-bs2/package/snippets/resources_list.html @@ -1,8 +1,9 @@ {# Renders a list of resources with icons and view links. -resources - A list of resources to render -pkg - A package object that the resources belong to. +resources - A list of resources (dicts) to render +pkg - A package dict that the resources belong to. +is_activity_archive - Whether this is an old version of the dataset (and therefore read-only) Example: @@ -15,14 +16,14 @@

    {{ _('Data and Resources') }}

    {% if resources %}
      {% block resource_list_inner %} - {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} + {% set can_edit = h.check_access('package_update', {'id':pkg.id }) and not is_activity_archive %} {% for resource in resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit, is_activity_archive=is_activity_archive %} {% endfor %} {% endblock %}
    {% else %} - {% if h.check_access('resource_create', {'package_id': pkg['id']}) %} + {% if h.check_access('resource_create', {'package_id': pkg['id']}) and not is_activity_archive %} {% trans url=h.url_for('resource.new', id=pkg.name) %}

    This dataset has no data, why not add some?

    {% endtrans %} diff --git a/ckan/templates-bs2/package/snippets/revisions_table.html b/ckan/templates-bs2/package/snippets/revisions_table.html deleted file mode 100644 index aebdf222b00..00000000000 --- a/ckan/templates-bs2/package/snippets/revisions_table.html +++ /dev/null @@ -1,31 +0,0 @@ -{% import 'macros/form.html' as form %} - - - - - - - - - - - - - {% for rev in pkg_revisions %} - - - - - - - - {% endfor %} - -
    {{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Log Message') }}
    - {{ h.radio('selected1', rev.id, checked=(loop.first)) }} - {{ h.radio('selected2', rev.id, checked=(loop.last)) }} - - {% link_for rev.id | truncate(6), controller='revision', action='read', id=rev.id %} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }}{{ rev.message }}
    \ No newline at end of file diff --git a/ckan/templates-bs2/package/snippets/tags.html b/ckan/templates-bs2/package/snippets/tags.html index 8d296d62ea2..bbb1f606533 100644 --- a/ckan/templates-bs2/package/snippets/tags.html +++ b/ckan/templates-bs2/package/snippets/tags.html @@ -1,3 +1,13 @@ +{# + Renders tags + + tags - a list of tag dicts + + Example: + + {% snippet "package/snippets/tags.html", tags=pkg.tags %} + +#} {% if tags %}
    {% snippet 'snippets/tag_list.html', tags=tags, _class='tag-list well' %} diff --git a/ckan/templates-bs2/revision/diff.html b/ckan/templates-bs2/revision/diff.html deleted file mode 100644 index bb516e73773..00000000000 --- a/ckan/templates-bs2/revision/diff.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% set pkg = c.pkg %} -{% set group = c.group %} - -{% block subtitle %}{{ _('Differences')}}{% endblock %} - -{% block breadcrumb_content %} - {% if c.diff_entity == 'package' %} - {% set dataset = pkg.title or pkg.name %} -
  • {% link_for _('Datasets'), named_route='dataset.search', highlight_actions = 'new index' %}
  • -
  • {% link_for dataset, named_route='dataset.read', id=pkg.name %}
  • -
  • {{ _('Revision Differences') }}
  • - {% elif c.diff_entity == 'group' %} - {% set group = group.display_name or group.name %} -
  • {% link_for _('Groups'), controller='group', action='index' %}
  • -
  • {% link_for group, controller='group', action='read', id=group.name %}
  • -
  • {{ _('Revision Differences') }}
  • - {% endif %} -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision Differences') }} - - {% if c.diff_entity == 'package' %} - {% link_for pkg.title, named_route='dataset.read', id=pkg.name %} - {% elif c.diff_entity == 'group' %} - {% link_for group.display_name, controller='group', action='read', id=group.name %} - {% endif %} -

    - -

    - From: {% link_for c.revision_from.id, controller='revision', action='read', id=c.revision_from.id %} - - {{ h.render_datetime(c.revision_from.timestamp, with_hours=True) }} -

    -

    - To: {% link_for c.revision_to.id, controller='revision', action='read', id=c.revision_to.id %} - - {{ h.render_datetime(c.revision_to.timestamp, with_hours=True) }} -

    - - {% if c.diff %} - - - - - - {% for field, diff in c.diff %} - - - - - {% endfor %} -
    {{ _('Field') }}{{ _('Difference') }}
    {{ field }}
    {{ diff }}
    - {% else %} -

    {{ _('No Differences') }}

    - {% endif %} -{% endblock %} diff --git a/ckan/templates-bs2/revision/list.html b/ckan/templates-bs2/revision/list.html deleted file mode 100644 index 84200a0822a..00000000000 --- a/ckan/templates-bs2/revision/list.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% block subtitle %}{{ _('Revision History') }}{% endblock %} - -{% block breadcrumb_content %} -
  • {{ _('Revisions') }}
  • -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision History') }}

    - - {{ c.page.pager() }} - - {% block revisions_list %} - {% snippet "revision/snippets/revisions_list.html", revisions=c.page.items %} - {% endblock %} - - {{ c.page.pager() }} -{% endblock %} diff --git a/ckan/templates-bs2/revision/read.html b/ckan/templates-bs2/revision/read.html deleted file mode 100644 index fb4e541802d..00000000000 --- a/ckan/templates-bs2/revision/read.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% set rev = c.revision %} - -{% block subtitle %}{{ _('Revision') }} {{ rev.id }}{% endblock %} - -{% block breadcrumb_content %} -
  • {% link_for _('Revisions'), controller='revision', action='index' %}
  • -
  • {{ rev.id |truncate(35) }}
  • -{% endblock %} - -{% block actions_content %} - {% if c.revision_change_state_allowed %} -
    -
  • - {% if rev.state != 'deleted' %} - - {% endif %} - {% if rev.state == 'deleted' %} - - {% endif %} -
  • -
    - {% endif %} -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision') }}: {{ rev.id }}

    - -
    -
    - {% if rev.state != 'active' %} -

    - {{ rev.state }} -

    - {% endif %} - -

    - {{ _('Author') }}: {{ h.linked_user(rev.author) }} -

    -

    - {{ _('Timestamp') }}: {{ h.render_datetime(rev.timestamp, with_hours=True) }} -

    -

    - {{ _('Log Message') }}: -

    -

    - {{ rev.message }} -

    -
    - -
    -

    {{ _('Changes') }}

    -

    {{ _('Datasets') }}

    -
      - {% for pkg in c.packages %} -
    • - {{ h.link_to(pkg.name, h.url_for('dataset.read', id=pkg.name)) }} -
    • - {% endfor %} -
    - -

    {{ _('Datasets\' Tags') }}

    -
      - {% for pkgtag in c.pkgtags %} -
    • - Dataset - {{ h.link_to(pkgtag.package.name, h.url_for('dataset.read', id=pkgtag.package.name)) }}, - Tag - {{ h.link_to(pkgtag.tag.name, h.url_for(controller='tag', action='read', id=pkgtag.tag.name)) }} -
    • - {% endfor %} -
    - -

    {{ _('Groups') }}

    -
      - {% for group in c.groups %} -
    • - {{ h.link_to(group.name, h.url_for(controller='group', action='read', id=group.name)) }} -
    • - {% endfor %} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/ckan/templates-bs2/revision/read_base.html b/ckan/templates-bs2/revision/read_base.html deleted file mode 100644 index 880e4323264..00000000000 --- a/ckan/templates-bs2/revision/read_base.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "page.html" %} - -{% block secondary_content %} - - {% block secondary_help_content %}{% endblock %} - - {% block package_social %} - {% snippet "snippets/social.html" %} - {% endblock %} - -{% endblock %} - -{% block primary_content %} -
    -
    - {% block primary_content_inner %}{% endblock %} -
    -
    -{% endblock %} \ No newline at end of file diff --git a/ckan/templates-bs2/revision/snippets/revisions_list.html b/ckan/templates-bs2/revision/snippets/revisions_list.html deleted file mode 100644 index 123040003e7..00000000000 --- a/ckan/templates-bs2/revision/snippets/revisions_list.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - {% for rev in revisions %} - - - - - - - - {% endfor %} - -
    {{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Entity') }}{{ _('Log Message') }}
    - {{rev.id | truncate(6)}} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }} - {% for pkg in rev.packages %} - {{ pkg.title }} - {% endfor %} - {% for group in rev.groups %} - {{ group.display_name }} - {% endfor %} - {{ rev.message }}
    diff --git a/ckan/templates-bs2/snippets/activities/added_tag.html b/ckan/templates-bs2/snippets/activities/added_tag.html new file mode 100644 index 00000000000..d9969d3c3f2 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/added_tag.html @@ -0,0 +1,14 @@ +
  • + +

    + {{ _('{actor} added the tag {tag} to the dataset {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity), + tag=ah.tag(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/changed_group.html b/ckan/templates-bs2/snippets/activities/changed_group.html new file mode 100644 index 00000000000..19825576639 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_group.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} updated the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/changed_organization.html b/ckan/templates-bs2/snippets/activities/changed_organization.html new file mode 100644 index 00000000000..073f8047318 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_organization.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} updated the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/changed_package.html b/ckan/templates-bs2/snippets/activities/changed_package.html new file mode 100644 index 00000000000..4bef2b6d5fa --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_package.html @@ -0,0 +1,23 @@ +
  • + +

    + {{ _('{actor} updated the dataset {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + +  |  + + {{ _('Changes') }} + + {% endif %} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/changed_resource.html b/ckan/templates-bs2/snippets/activities/changed_resource.html new file mode 100644 index 00000000000..28192bb62ee --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_resource.html @@ -0,0 +1,14 @@ +
  • + +

    + {{ _('{actor} updated the resource {resource} in the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.datset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/changed_user.html b/ckan/templates-bs2/snippets/activities/changed_user.html new file mode 100644 index 00000000000..c4ea1aa2155 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_user.html @@ -0,0 +1,12 @@ +
  • + +

    + {{ _('{actor} updated their profile').format( + actor=ah.actor(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/deleted_group.html b/ckan/templates-bs2/snippets/activities/deleted_group.html new file mode 100644 index 00000000000..bb5f92b3ae6 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/deleted_group.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} deleted the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/deleted_organization.html b/ckan/templates-bs2/snippets/activities/deleted_organization.html new file mode 100644 index 00000000000..27753e9151b --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/deleted_organization.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} deleted the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/deleted_package.html b/ckan/templates-bs2/snippets/activities/deleted_package.html new file mode 100644 index 00000000000..5731125f177 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/deleted_package.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} deleted the dataset {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/deleted_resource.html b/ckan/templates-bs2/snippets/activities/deleted_resource.html new file mode 100644 index 00000000000..d78d21234b1 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/deleted_resource.html @@ -0,0 +1,14 @@ +
  • + +

    + {{ _('{actor} deleted the resource {resource} from the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/fallback.html b/ckan/templates-bs2/snippets/activities/fallback.html new file mode 100644 index 00000000000..b119c295b36 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/fallback.html @@ -0,0 +1,37 @@ +{# + Fallback template for displaying an activity. + It's not pretty, but it is better than TemplateNotFound. + + Params: + activity - the Activity dict + can_show_activity_detail - whether we should render detail about the activity (i.e. "as it was" and diff, alternatively will just display the metadata about the activity) + id - the id or current name of the object (e.g. package name, user id) + ah - dict of template macros to render linked: actor, dataset, organization, user, group +#} +
  • + +

    + {{ _('{actor} {activity_type}').format( + actor=ah.actor(activity), + activity_type=activity.activity_type + )|safe }} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.group %} + {# do our best to differentiate between org & group #} + {% if 'group' in activity.activity_type %} + {{ ah.group(activity) }} + {% else %} + {{ ah.organization(activity) }} + {% endif %} + {% endif %} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/follow_dataset.html b/ckan/templates-bs2/snippets/activities/follow_dataset.html new file mode 100644 index 00000000000..f4487e4fdc0 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/follow_dataset.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} started following {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/follow_group.html b/ckan/templates-bs2/snippets/activities/follow_group.html new file mode 100644 index 00000000000..30a4bb12447 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/follow_group.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} started following {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/follow_user.html b/ckan/templates-bs2/snippets/activities/follow_user.html new file mode 100644 index 00000000000..9259ee49079 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/follow_user.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} started following {user}').format( + actor=ah.actor(activity), + user=ah.user(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/new_group.html b/ckan/templates-bs2/snippets/activities/new_group.html new file mode 100644 index 00000000000..50601aba535 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_group.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} created the group {group}').format( + actor=ah.actor(activity), + group=ah.group(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/new_organization.html b/ckan/templates-bs2/snippets/activities/new_organization.html new file mode 100644 index 00000000000..ef191615f53 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_organization.html @@ -0,0 +1,13 @@ +
  • + +

    + {{ _('{actor} created the organization {organization}').format( + actor=ah.actor(activity), + organization=ah.organization(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/new_package.html b/ckan/templates-bs2/snippets/activities/new_package.html new file mode 100644 index 00000000000..2d5cc5ab191 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_package.html @@ -0,0 +1,19 @@ +
  • + +

    + {{ _('{actor} created the dataset {dataset}').format( + actor=ah.actor(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/new_resource.html b/ckan/templates-bs2/snippets/activities/new_resource.html new file mode 100644 index 00000000000..d3f33c9882b --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_resource.html @@ -0,0 +1,14 @@ +
  • + +

    + {{ _('{actor} added the resource {resource} to the dataset {dataset}').format( + actor=ah.actor(activity), + resource=ah.resource(activity), + dataset=ah.dataset(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/new_user.html b/ckan/templates-bs2/snippets/activities/new_user.html new file mode 100644 index 00000000000..1293f146c7b --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_user.html @@ -0,0 +1,12 @@ +
  • + +

    + {{ _('{actor} signed up').format( + actor=ah.actor(activity) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activities/removed_tag.html b/ckan/templates-bs2/snippets/activities/removed_tag.html new file mode 100644 index 00000000000..00acfa15ff6 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/removed_tag.html @@ -0,0 +1,14 @@ +
  • + +

    + {{ _('{actor} removed the tag {tag} from the dataset {dataset}').format( + actor=ah.actor(activity), + tag=ah.tag(activity), + dataset=ah.dataset(dataset) + )|safe }} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates-bs2/snippets/activity_item.html b/ckan/templates-bs2/snippets/activity_item.html deleted file mode 100644 index 46619123479..00000000000 --- a/ckan/templates-bs2/snippets/activity_item.html +++ /dev/null @@ -1,10 +0,0 @@ -
  • - {% if activity.is_new %} - {{ _('New activity item') }} - {% endif %} - -

    - {{ h.literal(activity.msg.format(**activity.data)) }} - {{ h.time_ago_from_timestamp(activity.timestamp) }} -

    -
  • diff --git a/ckan/templates-bs2/snippets/activity_stream.html b/ckan/templates-bs2/snippets/activity_stream.html index 6d6a1d7983f..640d7eebc44 100644 --- a/ckan/templates-bs2/snippets/activity_stream.html +++ b/ckan/templates-bs2/snippets/activity_stream.html @@ -41,10 +41,12 @@ #} {% block activity_stream %}
      + {% set can_show_activity_detail = h.check_access('activity_list', {'id': id, 'include_data': True, 'object_type': object_type}) %} {% for activity in activity_stream %} {%- snippet "snippets/activities/{}.html".format( activity.activity_type.replace(' ', '_') - ), activity=activity, ah={ + ), "snippets/activities/fallback.html", + activity=activity, can_show_activity_detail=can_show_activity_detail, ah={ 'actor': actor, 'dataset': dataset, 'organization': organization, diff --git a/ckan/templates-bs2/snippets/tag_list.html b/ckan/templates-bs2/snippets/tag_list.html index 253220b1e21..929141ec1bf 100644 --- a/ckan/templates-bs2/snippets/tag_list.html +++ b/ckan/templates-bs2/snippets/tag_list.html @@ -1,6 +1,12 @@ {# -render a list of tags linking to the dataset search page -tags: list of tags + Render a list of tags linking to the dataset search page + + tags - list of tag dicts + + Example: + + {% snippet 'snippets/tag_list.html', tags=tags, _class='tag-list well' %} + #} {% set _class = _class or 'tag-list' %} {% block tag_list %} diff --git a/ckan/templates/group/history.html b/ckan/templates/group/history.html deleted file mode 100644 index 3481770ef85..00000000000 --- a/ckan/templates/group/history.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "group/read_base.html" %} - -{% block subtitle %}{{ _('History') }} {{ g.template_title_delimiter }} {{ group_dict.display_name }}{% endblock %} - -{% block primary_content_inner %} -

      {{ _('History') }}

      - {% block group_history_revisions %} - {% snippet "group/snippets/history_revisions.html", group_dict=group_dict, group_revisions=group_revisions %} - {% endblock %} -{% endblock %} diff --git a/ckan/templates/group/snippets/history_revisions.html b/ckan/templates/group/snippets/history_revisions.html deleted file mode 100644 index f7cb63fed21..00000000000 --- a/ckan/templates/group/snippets/history_revisions.html +++ /dev/null @@ -1,12 +0,0 @@ -{% import 'macros/form.html' as form %} - -
      - - {{ form.errors(error_summary) }} - - - {% snippet 'group/snippets/revisions_table.html', group_dict=group_dict, group_revisions=group_revisions %} - - - -
      \ No newline at end of file diff --git a/ckan/templates/group/snippets/revisions_table.html b/ckan/templates/group/snippets/revisions_table.html deleted file mode 100644 index 5b7f2eeec66..00000000000 --- a/ckan/templates/group/snippets/revisions_table.html +++ /dev/null @@ -1,31 +0,0 @@ -{% import 'macros/form.html' as form %} - - - - - - - - - - - - - {% for rev in group_revisions %} - - - - - - - - {% endfor %} - -
      {{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Log Message') }}
      - {{ h.radio('selected1', rev.id, checked=(loop.first)) }} - {{ h.radio('selected2', rev.id, checked=(loop.last)) }} - - {% link_for rev.id | truncate(6), controller='revision', action='read', id=rev.id %} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }}{{ rev.message }}
      \ No newline at end of file diff --git a/ckan/templates/macros/form.html b/ckan/templates/macros/form.html index 0a31b97b506..dd9a3fef6ff 100644 --- a/ckan/templates/macros/form.html +++ b/ckan/templates/macros/form.html @@ -51,7 +51,7 @@ {{ extra_html }} diff --git a/ckan/templates/package/base.html b/ckan/templates/package/base.html index aee47d83551..b0864a45a75 100644 --- a/ckan/templates/package/base.html +++ b/ckan/templates/package/base.html @@ -17,7 +17,7 @@ {% else %}
    • {% link_for _('Datasets'), named_route='dataset.search' %}
    • {% endif %} - {% link_for dataset|truncate(30), named_route='dataset.read', id=pkg.name %} + {% link_for dataset|truncate(30), named_route='dataset.read', id=pkg.id if is_activity_archive else pkg.name %} {% else %}
    • {% link_for _('Datasets'), named_route='dataset.search' %}
    • {{ _('Create Dataset') }}
    • diff --git a/ckan/templates/package/base_form_page.html b/ckan/templates/package/base_form_page.html index 185878e9ad7..8736649e271 100644 --- a/ckan/templates/package/base_form_page.html +++ b/ckan/templates/package/base_form_page.html @@ -34,6 +34,6 @@

      {{ _('What are data {% block resources_module %} {# TODO: Pass in a list of previously created resources and the current package dict #} - {% snippet "package/snippets/resources.html", pkg={}, action='new_resource' %} + {% snippet "package/snippets/resources.html", pkg={}, action='new_resource', is_activity_archive=False %} {% endblock %} {% endblock %} diff --git a/ckan/templates/package/changes.html b/ckan/templates/package/changes.html new file mode 100644 index 00000000000..9d8a40841c1 --- /dev/null +++ b/ckan/templates/package/changes.html @@ -0,0 +1,36 @@ +{% extends "package/base.html" %} + +{% block subtitle %}{{ pkg_dict.name }} {{ g.template_title_delimiter }} Changes {{ g.template_title_delimiter }} {{ super() }}{% endblock %} + +{% block breadcrumb_content_selected %}{% endblock %} + +{% block breadcrumb_content %} + {{ super() }} +
    • {% link_for _('Changes'), controller='package', action='activity', id=pkg_dict.name %}
    • +
    • {% link_for activity_diff.activities[1].id|truncate(30), controller='package', action='changes', activity_id=activity_diff.activities[1].id %}
    • +{% endblock %} + +{% block primary %} +
      +
      + {% block package_changes_header %} +

      {{ _('Changes') }}

      + Dataset: {% link_for pkg_dict.title, controller='package', action='read', id=pkg_dict.name %} + {% if activity_diff.activities[1].data.package.title != pkg_dict.title %} + (which had title "{{ activity_diff.activities[1].data.package.title }}" at the time) + {% endif %}
      + User: {{ h.linked_user(activity_diff.activities[1].user_id) }}
      + Date: {{ h.render_datetime(activity_diff.activities[1].timestamp, with_hours=True, with_seconds=True) }}
      + Compared to previous version from date: {{ h.render_datetime(activity_diff.activities[0].timestamp, with_hours=True, with_seconds=True) }})
      + {% endblock %} + + {% block package_changes_diff %} +
      +          {{ activity_diff['diff']|safe }}
      +        
      + {% endblock %} +
      +
      +{% endblock %} + +{% block secondary %}{% endblock %} diff --git a/ckan/templates/package/history.html b/ckan/templates/package/history.html deleted file mode 100644 index a4e81d4821b..00000000000 --- a/ckan/templates/package/history.html +++ /dev/null @@ -1,10 +0,0 @@ -{% extends "package/read_base.html" %} - -{% block subtitle %}{{ _('History') }} {{ g.template_title_delimiter }} {{ h.dataset_display_name(c.pkg_dict) }}{% endblock %} - -{% block primary_content_inner %} -

      {{ _('History') }}

      - {% block package_history_revisions %} - {% snippet "package/snippets/history_revisions.html", pkg_dict=pkg, pkg_revisions=c.pkg_revisions %} - {% endblock %} -{% endblock %} diff --git a/ckan/templates/package/new_resource.html b/ckan/templates/package/new_resource.html index 19518c74d8a..4abd5ba840c 100644 --- a/ckan/templates/package/new_resource.html +++ b/ckan/templates/package/new_resource.html @@ -12,7 +12,7 @@ {% 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, allow_upload=g.ofs_impl and logged_in, dataset_type=dataset_type %}{% 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 %} {% block secondary_content %} {% snippet 'package/snippets/resource_help.html' %} diff --git a/ckan/templates/package/new_resource_not_draft.html b/ckan/templates/package/new_resource_not_draft.html index e2ef4cb2e24..261b71ea0b3 100644 --- a/ckan/templates/package/new_resource_not_draft.html +++ b/ckan/templates/package/new_resource_not_draft.html @@ -9,7 +9,7 @@ {% endblock %} {% block form %} - {% snippet resource_form_snippet, data=data, errors=errors, error_summary=error_summary, include_metadata=false, pkg_name=pkg_name, stage=stage, allow_upload=g.ofs_impl and logged_in, dataset_type=dataset_type %} + {% 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 %} {% block content_primary_nav %} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 8246e2cc6ba..32c3c8b7cfb 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -9,6 +9,16 @@ {{ _('Private') }} {% endif %} + {% block package_archive_notice %} + {% if is_activity_archive %} +
      + {% trans url=h.url_for('dataset.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. To see the + current version, click here. + {% endtrans %} +
      + {% endif %} + {% endblock %}

      {% block page_heading %} {{ h.dataset_display_name(pkg) }} @@ -32,7 +42,7 @@

      {% endblock %} {% block package_resources %} - {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources %} + {% snippet "package/snippets/resources_list.html", pkg=pkg, resources=pkg.resources, is_activity_archive=is_activity_archive %} {% endblock %} {% block package_tags %} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index 0e47a02b5d7..0f0a458e791 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -10,30 +10,17 @@ {% endblock -%} {% block content_action %} - {% if h.check_access('package_update', {'id':pkg.id }) %} - {% link_for _('Manage'), named_route='dataset.edit', id=pkg.name, class_='btn btn-default', icon='wrench' %} + {% if not is_activity_archive %} + {% if h.check_access('package_update', {'id':pkg.id }) %} + {% link_for _('Manage'), named_route='dataset.edit', id=pkg.name, class_='btn btn-default', icon='wrench' %} + {% endif %} {% endif %} {% endblock %} {% block content_primary_nav %} - {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.name) }} - {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.name) }} - {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.name) }} -{% endblock %} - -{% block primary_content_inner %} - {% block package_revision_info %} - {% if c.revision_date %} -
      -

      - {% set timestamp = h.render_datetime(c.revision_date, with_hours=True) %} - {% set url = h.url_for('dataset.read', id=pkg.name) %} - - {% trans timestamp=timestamp, url=url %}This is an old revision of this dataset, as edited at {{ timestamp }}. It may differ significantly from the current revision.{% endtrans %} -

      -
      - {% endif %} - {% endblock %} + {{ h.build_nav_icon('dataset.read', _('Dataset'), id=pkg.id if is_activity_archive else pkg.name) }} + {{ h.build_nav_icon('dataset.groups', _('Groups'), id=pkg.id if is_activity_archive else pkg.name) }} + {{ h.build_nav_icon('dataset.activity', _('Activity Stream'), id=pkg.id if is_activity_archive else pkg.name) }} {% endblock %} {% block secondary_content %} @@ -46,7 +33,7 @@ {% block package_organization %} {% if pkg.organization %} - {% set org = h.get_organization(pkg.organization.name) %} + {% set org = h.get_organization(pkg.organization.id) %} {% snippet "snippets/organization.html", organization=org, has_context_title=true %} {% endif %} {% endblock %} diff --git a/ckan/templates/package/resource_edit.html b/ckan/templates/package/resource_edit.html index 6acb7929467..cb482bdb19d 100644 --- a/ckan/templates/package/resource_edit.html +++ b/ckan/templates/package/resource_edit.html @@ -9,7 +9,6 @@ error_summary=error_summary, pkg_name=pkg.name, form_action=form_action, - allow_upload=g.ofs_impl and logged_in, resource_form_snippet=resource_form_snippet, dataset_type=dataset_type %} {% endblock %} diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index daed76adc5e..3d35b7664e6 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -27,7 +27,7 @@ {% block resource_actions %}
        {% block resource_actions_inner %} - {% if h.check_access('package_update', {'id':pkg.id }) %} + {% if h.check_access('package_update', {'id':pkg.id }) and not is_activity_archive %}
      • {% link_for _('Manage'), named_route='resource.edit', id=pkg.name, resource_id=res.id, class_='btn btn-default', icon='wrench' %}
      • {% endif %} {% if res.url and h.is_url(res.url) %} @@ -44,8 +44,8 @@ {{ _('Download') }} {% endif %} - {% block download_resource_button %} - {%if res.datastore_active %} + {% block download_resource_button %} + {% if res.datastore_active %} @@ -61,7 +61,8 @@ target="_blank">XML
      - {%endif%} {% endblock %} + {% endif %} + {% endblock %} {% endif %} @@ -70,6 +71,16 @@ {% endblock %} {% block resource_content %} + {% block package_archive_notice %} + {% if is_activity_archive %} +
      + {% trans url=h.url_for('dataset.read', id=pkg.id) %} + You're currently viewing an old version of this dataset. To see the + current version, click here. + {% endtrans %} +
      + {% endif %} + {% endblock %} {% block resource_read_title %}

      {{ h.resource_display_name(res) | truncate(50) }}

      {% endblock %} {% block resource_read_url %} {% if res.url and h.is_url(res.url) %} @@ -85,10 +96,11 @@ {% if not res.description and package.notes %}

      {{ _('From the dataset abstract') }}

      {{ h.markdown_extract(h.get_translated(package, 'notes')) }}
      -

      {% trans dataset=package.title, url=h.url_for('dataset.read', id=package['name']) %}Source: {{ dataset }}{% endtrans %} +

      {% trans dataset=package.title, url=h.url_for('dataset.read', id=package.id if is_activity_archive else package.name) %}Source: {{ dataset }}{% endtrans %} {% endif %} {% endblock %} + {% if not is_activity_archive %} {% block data_preview %} {% block resource_view %} {% block resource_view_nav %} @@ -148,6 +160,7 @@

      {{ _('From the dataset abstract') }}

      {% endblock %} {% endblock %} + {% endif %} {% endblock %}
    {% endblock %} @@ -203,7 +216,7 @@

    {{ _('Additional Information') }}

    {% block secondary_content %} {% block resources_list %} - {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id %} + {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, action='read', is_activity_archive=is_activity_archive %} {% endblock %} {% block resource_license %} diff --git a/ckan/templates/package/resources.html b/ckan/templates/package/resources.html index 8de6b1a2ec0..275b6a54551 100644 --- a/ckan/templates/package/resources.html +++ b/ckan/templates/package/resources.html @@ -13,7 +13,7 @@
      {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} {% for resource in pkg.resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit, is_activity_archive=False %} {% endfor %}
    {% else %} diff --git a/ckan/templates/package/snippets/history_revisions.html b/ckan/templates/package/snippets/history_revisions.html deleted file mode 100644 index 5266f7d5dd9..00000000000 --- a/ckan/templates/package/snippets/history_revisions.html +++ /dev/null @@ -1,12 +0,0 @@ -{% import 'macros/form.html' as form %} - -
    - - {{ form.errors(error_summary) }} - - - {% snippet 'package/snippets/revisions_table.html', pkg_dict=pkg_dict, pkg_revisions=pkg_revisions %} - - - -
    \ No newline at end of file diff --git a/ckan/templates/package/snippets/info.html b/ckan/templates/package/snippets/info.html index 995482432a0..20678ffc6a2 100644 --- a/ckan/templates/package/snippets/info.html +++ b/ckan/templates/package/snippets/info.html @@ -1,5 +1,5 @@ {# -Displays a sidebard module with information for given package +Displays a sidebar module with information for given package pkg - The package dict that owns the resources. diff --git a/ckan/templates/package/snippets/resource_item.html b/ckan/templates/package/snippets/resource_item.html index 09e50145fa3..2ff580f965f 100644 --- a/ckan/templates/package/snippets/resource_item.html +++ b/ckan/templates/package/snippets/resource_item.html @@ -1,11 +1,25 @@ +{# + Renders a single resource with icons and view links. + + res - A resource dict to render + pkg - A package dict that the resource belongs to + can_edit - Whether the user is allowed to edit the resource + url_is_edit - Whether the link to the resource should be to editing it (set to False to make the link view the resource) + is_activity_archive - Whether this is an old version of the dataset (and therefore read-only) + + Example: + + {% snippet "package/snippets/resource_item.html", res=resource, pkg_dict=pkg_dict, can_edit=True, url_is_edit=False %} + +#} {% set url_action = 'resource.edit' if url_is_edit and can_edit else 'resource.read' %} -{% set url = h.url_for(url_action, id=pkg.name, resource_id=res.id) %} +{% set url = h.url_for(url_action, id=pkg.id if is_activity_archive else pkg.name, resource_id=res.id, **({'activity_id': request.args['activity_id']} if 'activity_id' in request.args else {})) %}
  • {% block resource_item_title %} {{ h.resource_display_name(res) | truncate(50) }}{{ h.get_translated(res, 'format') }} - {{ h.popular('views', res.tracking_summary.total, min=10) }} + {{ h.popular('views', res.tracking_summary.total, min=10) if res.tracking_summary }} {% endblock %} {% block resource_item_description %} @@ -27,7 +41,7 @@ {% block resource_item_explore_links %}
  • - {% if res.has_views %} + {% if not is_activity_archive and res.has_views %} {{ _('Preview') }} {% else %} diff --git a/ckan/templates/package/snippets/resources.html b/ckan/templates/package/snippets/resources.html index 2a3ea0a346e..107ed255cd0 100644 --- a/ckan/templates/package/snippets/resources.html +++ b/ckan/templates/package/snippets/resources.html @@ -5,10 +5,11 @@ pkg - The package dict that owns the resources. active - The id of the currently displayed resource. action - The resource action to use (default: 'read', meaning route 'resource.read'). +is_activity_archive - Whether this is an old version of the resource (and therefore read-only) Example: -{% snippet "package/snippets/resources.html", pkg=pkg, active=res.id %} + {% snippet "package/snippets/resources.html", pkg=pkg, active=res.id, is_activity_archive=False %} #} {% set resources = pkg.resources or [] %} @@ -23,7 +24,7 @@

    {{ _("Resources") }} {% for resource in resources %} {% endfor %} diff --git a/ckan/templates/package/snippets/resources_list.html b/ckan/templates/package/snippets/resources_list.html index e7b8e8ed9df..8c33546d6ca 100644 --- a/ckan/templates/package/snippets/resources_list.html +++ b/ckan/templates/package/snippets/resources_list.html @@ -1,8 +1,9 @@ {# Renders a list of resources with icons and view links. -resources - A list of resources to render -pkg - A package object that the resources belong to. +resources - A list of resources (dicts) to render +pkg - A package dict that the resources belong to. +is_activity_archive - Whether this is an old version of the dataset (and therefore read-only) Example: @@ -15,14 +16,14 @@

    {{ _('Data and Resources') }}

    {% if resources %}
      {% block resource_list_inner %} - {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} + {% set can_edit = h.check_access('package_update', {'id':pkg.id }) and not is_activity_archive %} {% for resource in resources %} - {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit %} + {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit, is_activity_archive=is_activity_archive %} {% endfor %} {% endblock %}
    {% else %} - {% if h.check_access('resource_create', {'package_id': pkg['id']}) %} + {% if h.check_access('resource_create', {'package_id': pkg['id']}) and not is_activity_archive %} {% trans url=h.url_for('resource.new', id=pkg.name) %}

    This dataset has no data, why not add some?

    {% endtrans %} diff --git a/ckan/templates/package/snippets/revisions_table.html b/ckan/templates/package/snippets/revisions_table.html deleted file mode 100644 index aebdf222b00..00000000000 --- a/ckan/templates/package/snippets/revisions_table.html +++ /dev/null @@ -1,31 +0,0 @@ -{% import 'macros/form.html' as form %} - - - - - - - - - - - - - {% for rev in pkg_revisions %} - - - - - - - - {% endfor %} - -
    {{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Log Message') }}
    - {{ h.radio('selected1', rev.id, checked=(loop.first)) }} - {{ h.radio('selected2', rev.id, checked=(loop.last)) }} - - {% link_for rev.id | truncate(6), controller='revision', action='read', id=rev.id %} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }}{{ rev.message }}
    \ No newline at end of file diff --git a/ckan/templates/package/snippets/tags.html b/ckan/templates/package/snippets/tags.html index 8d296d62ea2..bbb1f606533 100644 --- a/ckan/templates/package/snippets/tags.html +++ b/ckan/templates/package/snippets/tags.html @@ -1,3 +1,13 @@ +{# + Renders tags + + tags - a list of tag dicts + + Example: + + {% snippet "package/snippets/tags.html", tags=pkg.tags %} + +#} {% if tags %}
    {% snippet 'snippets/tag_list.html', tags=tags, _class='tag-list well' %} diff --git a/ckan/templates/revision/diff.html b/ckan/templates/revision/diff.html deleted file mode 100644 index 47d58bf1a63..00000000000 --- a/ckan/templates/revision/diff.html +++ /dev/null @@ -1,56 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% set pkg = c.pkg %} -{% set group = c.group %} - -{% block subtitle %}{{ _('Differences')}}{% endblock %} - -{% block breadcrumb_content %} - {% if c.diff_entity == 'package' %} - {% set dataset = pkg.title or pkg.name %} -
  • {% link_for _('Datasets'), 'dataset.search', highlight_actions = 'new index' %}
  • -
  • {% link_for dataset, named_route='dataset.read', id=pkg.name %}
  • -
  • {{ _('Revision Differences') }}
  • - {% elif c.diff_entity == 'group' %} - {% set group = group.display_name or group.name %} -
  • {% link_for _('Groups'), controller='group', action='index' %}
  • -
  • {% link_for group, controller='group', action='read', id=group.name %}
  • -
  • {{ _('Revision Differences') }}
  • - {% endif %} -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision Differences') }} - - {% if c.diff_entity == 'package' %} - {% link_for pkg.title, named_route='dataset.read', id=pkg.name %} - {% elif c.diff_entity == 'group' %} - {% link_for group.display_name, controller='group', action='read', id=group.name %} - {% endif %} -

    - -

    - From: {% link_for c.revision_from.id, controller='revision', action='read', id=c.revision_from.id %} - - {{ h.render_datetime(c.revision_from.timestamp, with_hours=True) }} -

    -

    - To: {% link_for c.revision_to.id, controller='revision', action='read', id=c.revision_to.id %} - - {{ h.render_datetime(c.revision_to.timestamp, with_hours=True) }} -

    - - {% if c.diff %} - - - - - - {% for field, diff in c.diff %} - - - - - {% endfor %} -
    {{ _('Field') }}{{ _('Difference') }}
    {{ field }}
    {{ diff }}
    - {% else %} -

    {{ _('No Differences') }}

    - {% endif %} -{% endblock %} diff --git a/ckan/templates/revision/list.html b/ckan/templates/revision/list.html deleted file mode 100644 index 84200a0822a..00000000000 --- a/ckan/templates/revision/list.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% block subtitle %}{{ _('Revision History') }}{% endblock %} - -{% block breadcrumb_content %} -
  • {{ _('Revisions') }}
  • -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision History') }}

    - - {{ c.page.pager() }} - - {% block revisions_list %} - {% snippet "revision/snippets/revisions_list.html", revisions=c.page.items %} - {% endblock %} - - {{ c.page.pager() }} -{% endblock %} diff --git a/ckan/templates/revision/read.html b/ckan/templates/revision/read.html deleted file mode 100644 index fb4e541802d..00000000000 --- a/ckan/templates/revision/read.html +++ /dev/null @@ -1,94 +0,0 @@ -{% extends "revision/read_base.html" %} - -{% set rev = c.revision %} - -{% block subtitle %}{{ _('Revision') }} {{ rev.id }}{% endblock %} - -{% block breadcrumb_content %} -
  • {% link_for _('Revisions'), controller='revision', action='index' %}
  • -
  • {{ rev.id |truncate(35) }}
  • -{% endblock %} - -{% block actions_content %} - {% if c.revision_change_state_allowed %} -
    -
  • - {% if rev.state != 'deleted' %} - - {% endif %} - {% if rev.state == 'deleted' %} - - {% endif %} -
  • -
    - {% endif %} -{% endblock %} - -{% block primary_content_inner %} -

    {{ _('Revision') }}: {{ rev.id }}

    - -
    -
    - {% if rev.state != 'active' %} -

    - {{ rev.state }} -

    - {% endif %} - -

    - {{ _('Author') }}: {{ h.linked_user(rev.author) }} -

    -

    - {{ _('Timestamp') }}: {{ h.render_datetime(rev.timestamp, with_hours=True) }} -

    -

    - {{ _('Log Message') }}: -

    -

    - {{ rev.message }} -

    -
    - -
    -

    {{ _('Changes') }}

    -

    {{ _('Datasets') }}

    -
      - {% for pkg in c.packages %} -
    • - {{ h.link_to(pkg.name, h.url_for('dataset.read', id=pkg.name)) }} -
    • - {% endfor %} -
    - -

    {{ _('Datasets\' Tags') }}

    -
      - {% for pkgtag in c.pkgtags %} -
    • - Dataset - {{ h.link_to(pkgtag.package.name, h.url_for('dataset.read', id=pkgtag.package.name)) }}, - Tag - {{ h.link_to(pkgtag.tag.name, h.url_for(controller='tag', action='read', id=pkgtag.tag.name)) }} -
    • - {% endfor %} -
    - -

    {{ _('Groups') }}

    -
      - {% for group in c.groups %} -
    • - {{ h.link_to(group.name, h.url_for(controller='group', action='read', id=group.name)) }} -
    • - {% endfor %} -
    -
    -
    -{% endblock %} \ No newline at end of file diff --git a/ckan/templates/revision/read_base.html b/ckan/templates/revision/read_base.html deleted file mode 100644 index 880e4323264..00000000000 --- a/ckan/templates/revision/read_base.html +++ /dev/null @@ -1,19 +0,0 @@ -{% extends "page.html" %} - -{% block secondary_content %} - - {% block secondary_help_content %}{% endblock %} - - {% block package_social %} - {% snippet "snippets/social.html" %} - {% endblock %} - -{% endblock %} - -{% block primary_content %} -
    -
    - {% block primary_content_inner %}{% endblock %} -
    -
    -{% endblock %} \ No newline at end of file diff --git a/ckan/templates/revision/snippets/revisions_list.html b/ckan/templates/revision/snippets/revisions_list.html deleted file mode 100644 index 123040003e7..00000000000 --- a/ckan/templates/revision/snippets/revisions_list.html +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - - - {% for rev in revisions %} - - - - - - - - {% endfor %} - -
    {{ _('Revision') }}{{ _('Timestamp') }}{{ _('Author') }}{{ _('Entity') }}{{ _('Log Message') }}
    - {{rev.id | truncate(6)}} - - {{ h.render_datetime(rev.timestamp, with_hours=True) }} - {{ h.linked_user(rev.author) }} - {% for pkg in rev.packages %} - {{ pkg.title }} - {% endfor %} - {% for group in rev.groups %} - {{ group.display_name }} - {% endfor %} - {{ rev.message }}
    diff --git a/ckan/templates/snippets/activities/fallback.html b/ckan/templates/snippets/activities/fallback.html new file mode 100644 index 00000000000..b119c295b36 --- /dev/null +++ b/ckan/templates/snippets/activities/fallback.html @@ -0,0 +1,37 @@ +{# + Fallback template for displaying an activity. + It's not pretty, but it is better than TemplateNotFound. + + Params: + activity - the Activity dict + can_show_activity_detail - whether we should render detail about the activity (i.e. "as it was" and diff, alternatively will just display the metadata about the activity) + id - the id or current name of the object (e.g. package name, user id) + ah - dict of template macros to render linked: actor, dataset, organization, user, group +#} +
  • + +

    + {{ _('{actor} {activity_type}').format( + actor=ah.actor(activity), + activity_type=activity.activity_type + )|safe }} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.package %} + {{ ah.dataset(activity) }} + {% endif %} + {% if activity.data.group %} + {# do our best to differentiate between org & group #} + {% if 'group' in activity.activity_type %} + {{ ah.group(activity) }} + {% else %} + {{ ah.organization(activity) }} + {% endif %} + {% endif %} +
    + + {{ h.time_ago_from_timestamp(activity.timestamp) }} + +

    +
  • diff --git a/ckan/templates/snippets/activity_item.html b/ckan/templates/snippets/activity_item.html deleted file mode 100644 index 46619123479..00000000000 --- a/ckan/templates/snippets/activity_item.html +++ /dev/null @@ -1,10 +0,0 @@ -
  • - {% if activity.is_new %} - {{ _('New activity item') }} - {% endif %} - -

    - {{ h.literal(activity.msg.format(**activity.data)) }} - {{ h.time_ago_from_timestamp(activity.timestamp) }} -

    -
  • diff --git a/ckan/templates/snippets/activity_stream.html b/ckan/templates/snippets/activity_stream.html index 6d6a1d7983f..640d7eebc44 100644 --- a/ckan/templates/snippets/activity_stream.html +++ b/ckan/templates/snippets/activity_stream.html @@ -41,10 +41,12 @@ #} {% block activity_stream %}