From 00edb1b862e21bf94c5bd7896217d17f17d1e2f1 Mon Sep 17 00:00:00 2001 From: Tyler Kennedy Date: Wed, 15 Mar 2017 10:28:30 -0400 Subject: [PATCH 001/103] Completely remove the revision controller. Remove the legacy revision controller 'tests' Activities are now just templates. Removes all _html 'helpers'. Remove leftover mentions of activity_streams.py and missed _html helpers from actions. Switch activity action based on group type. Fix 'View this version' links. Redirect legacy history page to activity feed (this page is not referenced by anything in core ckan) Revert a small change that breaks old fanstatic-based CKAN. Remove mentions to user_activity_list_html and _html test checks. Store complete package dict in activity. Store user name at time of change. Show historic package versions. Legacy tests do not include valid users when creating test packages. Ignore authentication when creating the package dict for the activity record. --- ckan/config/routing.py | 7 - ckan/controllers/group.py | 29 +- ckan/controllers/package.py | 140 ++-------- ckan/controllers/revision.py | 192 ------------- ckan/controllers/user.py | 45 +-- ckan/lib/activity_streams.py | 256 ------------------ ckan/lib/helpers.py | 10 +- ckan/logic/action/get.py | 186 +------------ ckan/model/package.py | 37 ++- ckan/templates/group/activity_stream.html | 10 +- .../organization/activity_stream.html | 10 +- ckan/templates/package/activity.html | 10 +- ckan/templates/package/read.html | 11 + ckan/templates/package/read_base.html | 6 +- ckan/templates/revision/diff.html | 56 ---- ckan/templates/revision/list.html | 19 -- ckan/templates/revision/read.html | 94 ------- ckan/templates/revision/read_base.html | 19 -- .../revision/snippets/revisions_list.html | 33 --- .../snippets/activities/added_tag.html | 14 + .../snippets/activities/changed_group.html | 13 + .../activities/changed_organization.html | 13 + .../snippets/activities/changed_package.html | 17 ++ .../snippets/activities/changed_resource.html | 14 + .../snippets/activities/changed_user.html | 12 + .../snippets/activities/deleted_group.html | 13 + .../activities/deleted_organization.html | 13 + .../snippets/activities/deleted_package.html | 13 + .../snippets/activities/deleted_resource.html | 14 + .../snippets/activities/follow_dataset.html | 13 + .../snippets/activities/follow_group.html | 13 + .../snippets/activities/follow_user.html | 13 + .../snippets/activities/new_group.html | 13 + .../snippets/activities/new_organization.html | 13 + .../snippets/activities/new_package.html | 17 ++ .../snippets/activities/new_resource.html | 14 + .../snippets/activities/new_user.html | 12 + .../snippets/activities/removed_tag.html | 14 + ckan/templates/snippets/activity_item.html | 10 - ckan/templates/snippets/activity_stream.html | 51 ++++ ckan/templates/user/activity_stream.html | 4 +- ckan/templates/user/dashboard.html | 4 +- ckan/tests/legacy/functional/test_package.py | 41 --- ckan/tests/legacy/functional/test_revision.py | 155 ----------- ckan/tests/logic/action/test_get.py | 5 - 45 files changed, 456 insertions(+), 1242 deletions(-) delete mode 100644 ckan/controllers/revision.py delete mode 100644 ckan/lib/activity_streams.py delete mode 100644 ckan/templates/revision/diff.html delete mode 100644 ckan/templates/revision/list.html delete mode 100644 ckan/templates/revision/read.html delete mode 100644 ckan/templates/revision/read_base.html delete mode 100644 ckan/templates/revision/snippets/revisions_list.html create mode 100644 ckan/templates/snippets/activities/added_tag.html create mode 100644 ckan/templates/snippets/activities/changed_group.html create mode 100644 ckan/templates/snippets/activities/changed_organization.html create mode 100644 ckan/templates/snippets/activities/changed_package.html create mode 100644 ckan/templates/snippets/activities/changed_resource.html create mode 100644 ckan/templates/snippets/activities/changed_user.html create mode 100644 ckan/templates/snippets/activities/deleted_group.html create mode 100644 ckan/templates/snippets/activities/deleted_organization.html create mode 100644 ckan/templates/snippets/activities/deleted_package.html create mode 100644 ckan/templates/snippets/activities/deleted_resource.html create mode 100644 ckan/templates/snippets/activities/follow_dataset.html create mode 100644 ckan/templates/snippets/activities/follow_group.html create mode 100644 ckan/templates/snippets/activities/follow_user.html create mode 100644 ckan/templates/snippets/activities/new_group.html create mode 100644 ckan/templates/snippets/activities/new_organization.html create mode 100644 ckan/templates/snippets/activities/new_package.html create mode 100644 ckan/templates/snippets/activities/new_resource.html create mode 100644 ckan/templates/snippets/activities/new_user.html create mode 100644 ckan/templates/snippets/activities/removed_tag.html delete mode 100644 ckan/templates/snippets/activity_item.html create mode 100644 ckan/templates/snippets/activity_stream.html delete mode 100644 ckan/tests/legacy/functional/test_revision.py diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 1e01114ea12..7f83e5ba473 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -394,13 +394,6 @@ def make_map(): ckan_icon='sitemap') m.connect('user_index', '/user', action='index') - 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') - # feeds with SubMapper(map, controller='feed') as m: m.connect('/feeds/group/{id}.atom', action='group') diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index e711fea8561..3d07a23a590 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -817,18 +817,23 @@ def activity(self, id, offset=0): except (NotFound, NotAuthorized): abort(404, _('Group not found')) - try: - # Add the group's activity stream (already rendered to HTML) to the - # template context for the group/read.html - # template to retrieve later. - c.group_activity_stream = self._action('group_activity_list_html')( - context, {'id': c.group_dict['id'], 'offset': offset}) - - except ValidationError as error: - base.abort(400) - - return render(self._activity_template(group_type), - extra_vars={'group_type': group_type}) + activity_action = 'group_activity_list' + if 'organization' in self.group_types: + activity_action = 'organization_activity_list' + + return render( + self._activity_template(group_type), + extra_vars={ + 'group_type': group_type, + 'activity_stream': get_action(activity_action)( + context, + { + 'id': c.group_dict['id'], + 'offset': offset + } + ) + } + ) def follow(self, id): '''Start following this group.''' diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index d57dee05e00..e898e3d2183 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -2,17 +2,15 @@ import logging from urllib import urlencode -import datetime import mimetypes import cgi -from ckan.common import config from paste.deploy.converters import asbool import paste.fileapp +from ckan.common import config import ckan.logic as logic import ckan.lib.base as base -import ckan.lib.i18n as i18n import ckan.lib.maintain as maintain import ckan.lib.navl.dictization_functions as dict_fns import ckan.lib.helpers as h @@ -343,29 +341,21 @@ def read(self, id): 'user': c.user, 'for_view': True, 'auth_user_obj': c.userobj} data_dict = {'id': id, 'include_tracking': True} - - # interpret @ or @ suffix - split = id.split('@') - if len(split) == 2: - data_dict['id'], revision_ref = split - if model.is_id(revision_ref): - context['revision_id'] = revision_ref - else: - try: - date = h.date_str_to_datetime(revision_ref) - context['revision_date'] = date - except TypeError, e: - abort(400, _('Invalid revision format: %r') % e.args) - except ValueError, e: - abort(400, _('Invalid revision format: %r') % e.args) - elif len(split) > 2: - abort(400, _('Invalid revision format: %r') % - 'Too many "@" symbols') + activity_id = request.params.get('activity_id') # check if package exists try: c.pkg_dict = get_action('package_show')(context, data_dict) c.pkg = context['package'] + + if activity_id: + c.pkg_dict = context['session'].query(model.Activity).get( + activity_id + ).data['package'] + # Don't crash on old activity records, which do not include + # resources or extras. + c.pkg_dict.setdefault('resources', []) + c.is_activity_archive = True except (NotFound, NotAuthorized): abort(404, _('Dataset not found')) @@ -388,8 +378,12 @@ def read(self, id): template = self._read_template(package_type) try: - return render(template, - extra_vars={'dataset_type': package_type}) + return render( + template, + extra_vars={ + 'dataset_type': package_type + } + ) except ckan.lib.render.TemplateNotFound: msg = _("Viewing {package_type} datasets in {format} format is " "not supported (template file {file} not found).".format( @@ -400,87 +394,7 @@ def read(self, id): assert False, "We should never get here" def history(self, id): - - if 'diff' in request.params or 'selected1' in request.params: - try: - params = {'id': request.params.getone('pkg_name'), - 'diff': request.params.getone('selected1'), - 'oldid': request.params.getone('selected2'), - } - except KeyError: - if 'pkg_name' in dict(request.params): - id = request.params.getone('pkg_name') - c.error = \ - _('Select two revisions before doing the comparison.') - else: - params['diff_entity'] = 'package' - h.redirect_to(controller='revision', action='diff', **params) - - context = {'model': model, 'session': model.Session, - 'user': c.user, 'auth_user_obj': c.userobj, - 'for_view': True} - data_dict = {'id': id} - try: - c.pkg_dict = get_action('package_show')(context, data_dict) - c.pkg_revisions = get_action('package_revision_list')(context, - data_dict) - # TODO: remove - # Still necessary for the authz check in group/layout.html - c.pkg = context['package'] - - except NotAuthorized: - abort(403, _('Unauthorized to read package %s') % '') - except NotFound: - abort(404, _('Dataset not found')) - - 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 Dataset Revision History'), - link=h.url_for(controller='revision', action='read', - id=c.pkg_dict['name']), - description=_(u'Recent changes to CKAN Dataset: ') + - (c.pkg_dict['title'] or ''), - language=unicode(i18n.get_lang()), - ) - for revision_dict in c.pkg_revisions: - revision_date = h.date_str_to_datetime( - revision_dict['timestamp']) - try: - dayHorizon = int(request.params.get('days')) - except: - dayHorizon = 30 - dayAge = (datetime.datetime.now() - revision_date).days - if dayAge >= dayHorizon: - break - if revision_dict['message']: - item_title = u'%s' % revision_dict['message'].\ - split('\n')[0] - else: - item_title = u'%s' % revision_dict['id'] - item_link = h.url_for(controller='revision', action='read', - id=revision_dict['id']) - item_description = _('Log message: ') - item_description += '%s' % (revision_dict['message'] or '') - item_author_name = revision_dict['author'] - item_pubdate = revision_date - feed.add_item( - title=item_title, - link=item_link, - description=item_description, - author_name=item_author_name, - pubdate=item_pubdate, - ) - response.headers['Content-Type'] = 'application/atom+xml' - return feed.writeString('utf-8') - - package_type = c.pkg_dict['type'] or 'dataset' - - return render( - self._history_template(c.pkg_dict.get('type', package_type)), - extra_vars={'dataset_type': package_type}) + h.redirect_to(controller='pacakge', action='activities', id=id) def new(self, data=None, errors=None, error_summary=None): if data and 'type' in data: @@ -1277,18 +1191,24 @@ def activity(self, id): data_dict = {'id': id} try: c.pkg_dict = get_action('package_show')(context, data_dict) - c.pkg = context['package'] - c.package_activity_stream = get_action( - 'package_activity_list_html')( - context, {'id': c.pkg_dict['id']}) dataset_type = c.pkg_dict['type'] or 'dataset' except NotFound: abort(404, _('Dataset not found')) except NotAuthorized: abort(403, _('Unauthorized to read dataset %s') % id) - return render('package/activity.html', - {'dataset_type': dataset_type}) + return render( + 'package/activity.html', + extra_vars={ + 'dataset_type': dataset_type, + 'activity_stream': get_action('package_activity_list')( + context, + { + 'id': id + } + ) + } + ) def resource_embedded_dataviewer(self, id, resource_id, width=500, height=500): diff --git a/ckan/controllers/revision.py b/ckan/controllers/revision.py deleted file mode 100644 index dd3527cc4ea..00000000000 --- a/ckan/controllers/revision.py +++ /dev/null @@ -1,192 +0,0 @@ -# encoding: utf-8 - -from datetime import datetime, timedelta - -from pylons.i18n import get_lang - -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=unicode(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] - package_extra_revisions = revision_changes[model.PackageExtra] - 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 - for package_extra_revision in package_extra_revisions: - if package_extra_revision.package_id == \ - package.id: - if package_extra_revision.key == \ - 'date_updated': - transition += ':date_updated' - 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/controllers/user.py b/ckan/controllers/user.py index 4c622caf243..04e2189fb2c 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -136,13 +136,6 @@ def read(self, id=None): 'include_num_followers': True} self._setup_template_variables(context, data_dict) - - # The legacy templates have the user's activity stream on the user - # profile page, new templates do not. - if asbool(config.get('ckan.legacy_templates', False)): - c.user_activity_stream = get_action('user_activity_list_html')( - context, {'id': c.user_dict['id']}) - return render('user/read.html') def me(self, locale=None): @@ -595,13 +588,18 @@ def activity(self, id, offset=0): self._setup_template_variables(context, data_dict) - try: - c.user_activity_stream = get_action('user_activity_list_html')( - context, {'id': c.user_dict['id'], 'offset': offset}) - except ValidationError: - base.abort(400) - - return render('user/activity_stream.html') + return render( + 'user/activity_stream.html', + extra_vars={ + 'activity_stream': get_action('user_activity_list')( + context, + { + 'id': id, + 'offset': offset + } + ) + } + ) def _get_dashboard_context(self, filter_type=None, filter_id=None, q=None): '''Return a dict needed by the dashboard view to determine context.''' @@ -668,10 +666,19 @@ def dashboard(self, id=None, offset=0): filter_id = request.params.get('name', u'') c.followee_list = get_action('followee_list')( - context, {'id': c.userobj.id, 'q': q}) + context, + { + 'id': c.userobj.id, + 'q': q + } + ) + c.dashboard_activity_stream_context = self._get_dashboard_context( - filter_type, filter_id, q) - c.dashboard_activity_stream = h.dashboard_activity_stream( + filter_type, + filter_id, + q + ) + dashboard_activity_stream = h.dashboard_activity_stream( c.userobj.id, filter_type, filter_id, offset ) @@ -679,7 +686,9 @@ def dashboard(self, id=None, offset=0): # dashboard page. get_action('dashboard_mark_activities_old')(context, {}) - return render('user/dashboard.html') + return render('user/dashboard.html', extra_vars={ + 'activity_stream': dashboard_activity_stream + }) def dashboard_datasets(self): context = {'for_view': True, 'user': c.user, diff --git a/ckan/lib/activity_streams.py b/ckan/lib/activity_streams.py deleted file mode 100644 index 481e7a696c7..00000000000 --- a/ckan/lib/activity_streams.py +++ /dev/null @@ -1,256 +0,0 @@ -# encoding: utf-8 - -import re - -from webhelpers.html import literal - -import ckan.lib.helpers as h -import ckan.lib.base as base -import ckan.logic as logic - -from ckan.common import _ - -# get_snippet_*() functions replace placeholders like {user}, {dataset}, etc. -# in activity strings with HTML representations of particular users, datasets, -# etc. - -def get_snippet_actor(activity, detail): - return literal('''%s''' - % (h.linked_user(activity['user_id'], 0, 30)) - ) - -def get_snippet_user(activity, detail): - return literal('''%s''' - % (h.linked_user(activity['object_id'], 0, 20)) - ) - -def get_snippet_dataset(activity, detail): - data = activity['data'] - pkg_dict = data.get('package') or data.get('dataset') - link = h.dataset_link(pkg_dict) if pkg_dict else '' - return literal('''%s''' - % (link) - ) - -def get_snippet_tag(activity, detail): - return h.tag_link(detail['data']['tag']) - -def get_snippet_group(activity, detail): - link = h.group_link(activity['data']['group']) - return literal('''%s''' - % (link) - ) - -def get_snippet_organization(activity, detail): - return h.organization_link(activity['data']['group']) - -def get_snippet_extra(activity, detail): - return '"%s"' % detail['data']['package_extra']['key'] - -def get_snippet_resource(activity, detail): - return h.resource_link(detail['data']['resource'], - activity['data']['package']['id']) - -# activity_stream_string_*() functions return translatable string -# representations of activity types, the strings contain placeholders like -# {user}, {dataset} etc. to be replaced with snippets from the get_snippet_*() -# functions above. - -def activity_stream_string_added_tag(context, activity): - return _("{actor} added the tag {tag} to the dataset {dataset}") - -def activity_stream_string_changed_group(context, activity): - return _("{actor} updated the group {group}") - -def activity_stream_string_changed_organization(context, activity): - return _("{actor} updated the organization {organization}") - -def activity_stream_string_changed_package(context, activity): - return _("{actor} updated the dataset {dataset}") - -def activity_stream_string_changed_package_extra(context, activity): - return _("{actor} changed the extra {extra} of the dataset {dataset}") - -def activity_stream_string_changed_resource(context, activity): - return _("{actor} updated the resource {resource} in the dataset {dataset}") - -def activity_stream_string_changed_user(context, activity): - return _("{actor} updated their profile") - -def activity_stream_string_deleted_group(context, activity): - return _("{actor} deleted the group {group}") - -def activity_stream_string_deleted_organization(context, activity): - return _("{actor} deleted the organization {organization}") - -def activity_stream_string_deleted_package(context, activity): - return _("{actor} deleted the dataset {dataset}") - -def activity_stream_string_deleted_package_extra(context, activity): - return _("{actor} deleted the extra {extra} from the dataset {dataset}") - -def activity_stream_string_deleted_resource(context, activity): - return _("{actor} deleted the resource {resource} from the dataset " - "{dataset}") - -def activity_stream_string_new_group(context, activity): - return _("{actor} created the group {group}") - -def activity_stream_string_new_organization(context, activity): - return _("{actor} created the organization {organization}") - -def activity_stream_string_new_package(context, activity): - return _("{actor} created the dataset {dataset}") - -def activity_stream_string_new_package_extra(context, activity): - return _("{actor} added the extra {extra} to the dataset {dataset}") - -def activity_stream_string_new_resource(context, activity): - return _("{actor} added the resource {resource} to the dataset {dataset}") - -def activity_stream_string_new_user(context, activity): - return _("{actor} signed up") - -def activity_stream_string_removed_tag(context, activity): - return _("{actor} removed the tag {tag} from the dataset {dataset}") - -def activity_stream_string_follow_dataset(context, activity): - return _("{actor} started following {dataset}") - -def activity_stream_string_follow_user(context, activity): - return _("{actor} started following {user}") - -def activity_stream_string_follow_group(context, activity): - return _("{actor} started following {group}") - -# A dictionary mapping activity snippets to functions that expand the snippets. -activity_snippet_functions = { - 'actor': get_snippet_actor, - 'user': get_snippet_user, - 'dataset': get_snippet_dataset, - 'tag': get_snippet_tag, - 'group': get_snippet_group, - 'organization': get_snippet_organization, - 'extra': get_snippet_extra, - 'resource': get_snippet_resource, -} - -# A dictionary mapping activity types to functions that return translatable -# string descriptions of the activity types. -activity_stream_string_functions = { - 'added tag': activity_stream_string_added_tag, - 'changed group': activity_stream_string_changed_group, - 'changed organization': activity_stream_string_changed_organization, - 'changed package': activity_stream_string_changed_package, - 'changed package_extra': activity_stream_string_changed_package_extra, - 'changed resource': activity_stream_string_changed_resource, - 'changed user': activity_stream_string_changed_user, - 'deleted group': activity_stream_string_deleted_group, - 'deleted organization': activity_stream_string_deleted_organization, - 'deleted package': activity_stream_string_deleted_package, - 'deleted package_extra': activity_stream_string_deleted_package_extra, - 'deleted resource': activity_stream_string_deleted_resource, - 'new group': activity_stream_string_new_group, - 'new organization': activity_stream_string_new_organization, - 'new package': activity_stream_string_new_package, - 'new package_extra': activity_stream_string_new_package_extra, - 'new resource': activity_stream_string_new_resource, - 'new user': activity_stream_string_new_user, - 'removed tag': activity_stream_string_removed_tag, - 'follow dataset': activity_stream_string_follow_dataset, - 'follow user': activity_stream_string_follow_user, - 'follow group': activity_stream_string_follow_group, -} - -# A dictionary mapping activity types to the icons associated to them -activity_stream_string_icons = { - 'added tag': 'tag', - 'changed group': 'users', - 'changed package': 'sitemap', - 'changed package_extra': 'pencil-square-o', - 'changed resource': 'file', - 'changed user': 'user', - 'deleted group': 'users', - 'deleted package': 'sitemap', - 'deleted package_extra': 'pencil-square-o', - 'deleted resource': 'file', - 'new group': 'users', - 'new package': 'sitemap', - 'new package_extra': 'pencil-square-o', - 'new resource': 'file', - 'new user': 'user', - 'removed tag': 'tag', - 'follow dataset': 'sitemap', - 'follow user': 'user', - 'follow group': 'users', - 'changed organization': 'briefcase', - 'deleted organization': 'briefcase', - 'new organization': 'briefcase', - 'undefined': 'certificate', # This is when no activity icon can be found -} - -# A list of activity types that may have details -activity_stream_actions_with_detail = ['changed package'] - -def activity_list_to_html(context, activity_stream, extra_vars): - '''Return the given activity stream as a snippet of HTML. - - :param activity_stream: the activity stream to render - :type activity_stream: list of activity dictionaries - :param extra_vars: extra variables to pass to the activity stream items - template when rendering it - :type extra_vars: dictionary - - :rtype: HTML-formatted string - - ''' - activity_list = [] # These are the activity stream messages. - for activity in activity_stream: - detail = None - activity_type = activity['activity_type'] - # Some activity types may have details. - if activity_type in activity_stream_actions_with_detail: - details = logic.get_action('activity_detail_list')(context=context, - data_dict={'id': activity['id']}) - # If an activity has just one activity detail then render the - # detail instead of the activity. - if len(details) == 1: - detail = details[0] - object_type = detail['object_type'] - - if object_type == 'PackageExtra': - object_type = 'package_extra' - - new_activity_type = '%s %s' % (detail['activity_type'], - object_type.lower()) - if new_activity_type in activity_stream_string_functions: - activity_type = new_activity_type - - if not activity_type in activity_stream_string_functions: - raise NotImplementedError("No activity renderer for activity " - "type '%s'" % activity_type) - - if activity_type in activity_stream_string_icons: - activity_icon = activity_stream_string_icons[activity_type] - else: - activity_icon = activity_stream_string_icons['undefined'] - - activity_msg = activity_stream_string_functions[activity_type](context, - activity) - - # Get the data needed to render the message. - matches = re.findall('\{([^}]*)\}', activity_msg) - data = {} - for match in matches: - snippet = activity_snippet_functions[match](activity, detail) - data[str(match)] = snippet - - activity_list.append({'msg': activity_msg, - 'type': activity_type.replace(' ', '-').lower(), - 'icon': activity_icon, - 'data': data, - 'timestamp': activity['timestamp'], - 'is_new': activity.get('is_new', False)}) - extra_vars['activities'] = activity_list - return literal(base.render('activity_streams/activity_stream_items.html', - extra_vars=extra_vars)) diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 15b4fca89bc..a6c57b3f38c 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1723,15 +1723,15 @@ def dashboard_activity_stream(user_id, filter_type=None, filter_id=None, if filter_type: action_functions = { - 'dataset': 'package_activity_list_html', - 'user': 'user_activity_list_html', - 'group': 'group_activity_list_html', - 'organization': 'organization_activity_list_html', + 'dataset': 'package_activity_list', + 'user': 'user_activity_list', + 'group': 'group_activity_list', + 'organization': 'organization_activity_list', } action_function = logic.get_action(action_functions.get(filter_type)) return action_function(context, {'id': filter_id, 'offset': offset}) else: - return logic.get_action('dashboard_activity_list_html')( + return logic.get_action('dashboard_activity_list')( context, {'offset': offset}) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index d7b92a29e9a..b863e7a564d 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -24,7 +24,6 @@ import ckan.plugins as plugins import ckan.lib.search as search import ckan.lib.plugins as lib_plugins -import ckan.lib.activity_streams as activity_streams import ckan.lib.datapreview as datapreview import ckan.authz as authz @@ -2637,10 +2636,12 @@ def organization_activity_list(context, data_dict): org_show = logic.get_action('organization_show') org_id = org_show(context, {'id': org_id})['id'] - _activity_objects = model.activity.group_activity_list(org_id, + activity_objects = model.activity.group_activity_list(org_id, 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) @@ -2692,154 +2693,6 @@ def activity_detail_list(context, data_dict): activity_detail_objects, context) -def user_activity_list_html(context, data_dict): - '''Return a user's public activity stream as HTML. - - The activity stream is rendered as a snippet of HTML meant to be included - in an HTML page, i.e. it doesn't have any HTML header or footer. - - :param id: The id or name of the user. - :type id: string - :param offset: where to start getting activity items from - (optional, default: 0) - :type offset: int - :param limit: the maximum number of activities to return - (optional, default: 31, the default value is configurable via the - ckan.activity_list_limit setting) - :type limit: int - - :rtype: string - - ''' - activity_stream = user_activity_list(context, data_dict) - offset = int(data_dict.get('offset', 0)) - extra_vars = { - 'controller': 'user', - 'action': 'activity', - 'id': data_dict['id'], - 'offset': offset, - } - return activity_streams.activity_list_to_html( - context, activity_stream, extra_vars) - - -def package_activity_list_html(context, data_dict): - '''Return a package's activity stream as HTML. - - The activity stream is rendered as a snippet of HTML meant to be included - in an HTML page, i.e. it doesn't have any HTML header or footer. - - :param id: the id or name of the package - :type id: string - :param offset: where to start getting activity items from - (optional, default: 0) - :type offset: int - :param limit: the maximum number of activities to return - (optional, default: 31, the default value is configurable via the - ckan.activity_list_limit setting) - :type limit: int - - :rtype: string - - ''' - activity_stream = package_activity_list(context, data_dict) - offset = int(data_dict.get('offset', 0)) - extra_vars = { - 'controller': 'package', - 'action': 'activity', - 'id': data_dict['id'], - 'offset': offset, - } - return activity_streams.activity_list_to_html( - context, activity_stream, extra_vars) - - -def group_activity_list_html(context, data_dict): - '''Return a group's activity stream as HTML. - - The activity stream is rendered as a snippet of HTML meant to be included - in an HTML page, i.e. it doesn't have any HTML header or footer. - - :param id: the id or name of the group - :type id: string - :param offset: where to start getting activity items from - (optional, default: 0) - :type offset: int - :param limit: the maximum number of activities to return - (optional, default: 31, the default value is configurable via the - ckan.activity_list_limit setting) - :type limit: int - - :rtype: string - - ''' - activity_stream = group_activity_list(context, data_dict) - offset = int(data_dict.get('offset', 0)) - extra_vars = { - 'controller': 'group', - 'action': 'activity', - 'id': data_dict['id'], - 'offset': offset, - } - return activity_streams.activity_list_to_html( - context, activity_stream, extra_vars) - - -def organization_activity_list_html(context, data_dict): - '''Return a organization's activity stream as HTML. - - The activity stream is rendered as a snippet of HTML meant to be included - in an HTML page, i.e. it doesn't have any HTML header or footer. - - :param id: the id or name of the organization - :type id: string - - :rtype: string - - ''' - activity_stream = organization_activity_list(context, data_dict) - offset = int(data_dict.get('offset', 0)) - extra_vars = { - 'controller': 'organization', - 'action': 'activity', - 'id': data_dict['id'], - 'offset': offset, - } - - return activity_streams.activity_list_to_html( - context, activity_stream, extra_vars) - - -def recently_changed_packages_activity_list_html(context, data_dict): - '''Return the activity stream of all recently changed packages as HTML. - - The activity stream includes all recently added or changed packages. It is - rendered as a snippet of HTML meant to be included in an HTML page, i.e. it - doesn't have any HTML header or footer. - - :param offset: where to start getting activity items from - (optional, default: 0) - :type offset: int - :param limit: the maximum number of activities to return - (optional, default: 31, the default value is configurable via the - ckan.activity_list_limit setting) - :type limit: int - - :rtype: string - - ''' - activity_stream = recently_changed_packages_activity_list( - context, data_dict) - offset = int(data_dict.get('offset', 0)) - extra_vars = { - 'controller': 'package', - 'action': 'activity', - 'offset': offset, - } - return activity_streams.activity_list_to_html( - context, activity_stream, extra_vars) - - def _follower_count(context, data_dict, default_schema, ModelClass): schema = context.get('schema', default_schema) data_dict, errors = _validate(data_dict, schema, context) @@ -3368,39 +3221,6 @@ def dashboard_activity_list(context, data_dict): return activity_dicts -@logic.validate(ckan.logic.schema.default_pagination_schema) -def dashboard_activity_list_html(context, data_dict): - '''Return the authorized (via login or API key) user's dashboard activity - stream as HTML. - - The activity stream is rendered as a snippet of HTML meant to be included - in an HTML page, i.e. it doesn't have any HTML header or footer. - - :param offset: where to start getting activity items from - (optional, default: 0) - :type offset: int - :param limit: the maximum number of activities to return - (optional, default: 31, the default value is configurable via the - ckan.activity_list_limit setting) - :type limit: int - - :rtype: string - - ''' - activity_stream = dashboard_activity_list(context, data_dict) - model = context['model'] - user_id = context['user'] - offset = data_dict.get('offset', 0) - extra_vars = { - 'controller': 'user', - 'action': 'dashboard', - 'offset': offset, - 'id': user_id - } - return activity_streams.activity_list_to_html(context, activity_stream, - extra_vars) - - def dashboard_new_activities_count(context, data_dict): '''Return the number of new activities in the user's dashboard. diff --git a/ckan/model/package.py b/ckan/model/package.py index 9ef68277330..416dff0a4e8 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -1,11 +1,10 @@ # encoding: utf-8 import datetime -from calendar import timegm import logging logger = logging.getLogger(__name__) -from sqlalchemy.sql import select, and_, union, or_ +from sqlalchemy.sql import and_, or_ from sqlalchemy import orm from sqlalchemy import types, Column, Table from ckan.common import config @@ -24,7 +23,7 @@ __all__ = ['Package', 'package_table', 'package_revision_table', 'PACKAGE_NAME_MAX_LENGTH', 'PACKAGE_NAME_MIN_LENGTH', - 'PACKAGE_VERSION_MAX_LENGTH', 'PackageTag', 'PackageTagRevision', + 'PACKAGE_VERSION_MAX_LENGTH', 'PackageTagRevision', 'PackageRevision'] @@ -508,6 +507,7 @@ def get_fields(core_only=False, fields_to_ignore=None): def activity_stream_item(self, activity_type, revision, user_id): import ckan.model import ckan.logic + assert activity_type in ("new", "changed"), ( str(activity_type)) @@ -527,10 +527,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': True, + 'ignore_auth': True + }, { + 'id': self.id, + 'include_tracking': True + }) except ckan.logic.NotFound: # This happens if this package is being purged and therefore has no # current revision. @@ -538,6 +545,22 @@ def activity_stream_item(self, activity_type, revision, user_id): # is purged. return None + 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. + # Legacy tests do not include valid users :( + 'actor': actor.name if actor else None + } + ) + def activity_stream_detail(self, activity_id, activity_type): import ckan.model diff --git a/ckan/templates/group/activity_stream.html b/ckan/templates/group/activity_stream.html index 47a6d029bb4..3353b20bd20 100644 --- a/ckan/templates/group/activity_stream.html +++ b/ckan/templates/group/activity_stream.html @@ -3,8 +3,10 @@ {% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %} -

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

- {% block activity_stream %} - {{ c.group_activity_stream | safe }} - {% endblock %} +

+ {% block page_heading %} + {{ _('Activity Stream') }} + {% endblock %} +

+ {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} {% endblock %} diff --git a/ckan/templates/organization/activity_stream.html b/ckan/templates/organization/activity_stream.html index ffa029d951b..d5ed8726153 100644 --- a/ckan/templates/organization/activity_stream.html +++ b/ckan/templates/organization/activity_stream.html @@ -3,8 +3,10 @@ {% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %} -

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

- {% block activity_stream %} - {{ c.group_activity_stream | safe }} - {% endblock %} +

+ {% block page_heading -%} + {{ _('Activity Stream') }} + {%- endblock %} +

+ {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} {% endblock %} diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index 3941a6dda02..b54cf190c04 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -3,8 +3,10 @@ {% block subtitle %}{{ _('Activity Stream') }} - {{ super() }}{% endblock %} {% block primary_content_inner %} -

{% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

- {% block activity_stream %} - {{ c.package_activity_stream | safe }} - {% endblock %} +

+ {% block page_heading %} + {{ _('Activity Stream') }} + {% endblock %} +

+ {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} {% endblock %} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 708e15083a1..df158a0c6e1 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -11,6 +11,17 @@ {{ _('Private') }} {% endif %} + {% block package_archive_notice %} + {% if c.is_activity_archive %} +
+ {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} + You're currently viewing an old version of this dataset. Some resources + may no longer exist or the dataset may not display correctly. To see the + current version, click here. + {% endtrans %} +
+ {% endif %} + {% endblock %}

{% block page_heading %} {{ h.dataset_display_name(pkg) }} diff --git a/ckan/templates/package/read_base.html b/ckan/templates/package/read_base.html index 448f31a8706..214f8c76582 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -10,8 +10,10 @@ {% endblock -%} {% block content_action %} - {% if h.check_access('package_update', {'id':pkg.id }) %} - {% link_for _('Manage'), controller='package', action='edit', id=pkg.name, class_='btn', icon='wrench' %} + {% if not c.is_activity_archive %} + {% if h.check_access('package_update', {'id':pkg.id }) %} + {% link_for _('Manage'), controller='package', action='edit', id=pkg.name, class_='btn', icon='wrench' %} + {% endif %} {% endif %} {% endblock %} diff --git a/ckan/templates/revision/diff.html b/ckan/templates/revision/diff.html deleted file mode 100644 index 6909da4286b..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'), controller='package', action='search', highlight_actions = 'new index' %}
  • -
  • {% link_for dataset, controller='package', action='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, controller='package', action='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 e0223288b67..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(controller='package', action='read', id=pkg.name)) }} -
    • - {% endfor %} -
    - -

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

    -
      - {% for pkgtag in c.pkgtags %} -
    • - Dataset - {{ h.link_to(pkgtag.package.name, h.url_for(controller='package', action='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 d63891581d6..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/added_tag.html b/ckan/templates/snippets/activities/added_tag.html new file mode 100644 index 00000000000..d9969d3c3f2 --- /dev/null +++ b/ckan/templates/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/snippets/activities/changed_group.html b/ckan/templates/snippets/activities/changed_group.html new file mode 100644 index 00000000000..19825576639 --- /dev/null +++ b/ckan/templates/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/snippets/activities/changed_organization.html b/ckan/templates/snippets/activities/changed_organization.html new file mode 100644 index 00000000000..073f8047318 --- /dev/null +++ b/ckan/templates/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/snippets/activities/changed_package.html b/ckan/templates/snippets/activities/changed_package.html new file mode 100644 index 00000000000..e78bd8bb952 --- /dev/null +++ b/ckan/templates/snippets/activities/changed_package.html @@ -0,0 +1,17 @@ +
  • + +

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

    +
  • diff --git a/ckan/templates/snippets/activities/changed_resource.html b/ckan/templates/snippets/activities/changed_resource.html new file mode 100644 index 00000000000..28192bb62ee --- /dev/null +++ b/ckan/templates/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/snippets/activities/changed_user.html b/ckan/templates/snippets/activities/changed_user.html new file mode 100644 index 00000000000..c4ea1aa2155 --- /dev/null +++ b/ckan/templates/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/snippets/activities/deleted_group.html b/ckan/templates/snippets/activities/deleted_group.html new file mode 100644 index 00000000000..bb5f92b3ae6 --- /dev/null +++ b/ckan/templates/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/snippets/activities/deleted_organization.html b/ckan/templates/snippets/activities/deleted_organization.html new file mode 100644 index 00000000000..27753e9151b --- /dev/null +++ b/ckan/templates/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/snippets/activities/deleted_package.html b/ckan/templates/snippets/activities/deleted_package.html new file mode 100644 index 00000000000..5731125f177 --- /dev/null +++ b/ckan/templates/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/snippets/activities/deleted_resource.html b/ckan/templates/snippets/activities/deleted_resource.html new file mode 100644 index 00000000000..d78d21234b1 --- /dev/null +++ b/ckan/templates/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/snippets/activities/follow_dataset.html b/ckan/templates/snippets/activities/follow_dataset.html new file mode 100644 index 00000000000..f4487e4fdc0 --- /dev/null +++ b/ckan/templates/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/snippets/activities/follow_group.html b/ckan/templates/snippets/activities/follow_group.html new file mode 100644 index 00000000000..30a4bb12447 --- /dev/null +++ b/ckan/templates/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/snippets/activities/follow_user.html b/ckan/templates/snippets/activities/follow_user.html new file mode 100644 index 00000000000..9259ee49079 --- /dev/null +++ b/ckan/templates/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/snippets/activities/new_group.html b/ckan/templates/snippets/activities/new_group.html new file mode 100644 index 00000000000..50601aba535 --- /dev/null +++ b/ckan/templates/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/snippets/activities/new_organization.html b/ckan/templates/snippets/activities/new_organization.html new file mode 100644 index 00000000000..ef191615f53 --- /dev/null +++ b/ckan/templates/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/snippets/activities/new_package.html b/ckan/templates/snippets/activities/new_package.html new file mode 100644 index 00000000000..fb421dcccf6 --- /dev/null +++ b/ckan/templates/snippets/activities/new_package.html @@ -0,0 +1,17 @@ +
  • + +

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

    +
  • diff --git a/ckan/templates/snippets/activities/new_resource.html b/ckan/templates/snippets/activities/new_resource.html new file mode 100644 index 00000000000..d3f33c9882b --- /dev/null +++ b/ckan/templates/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/snippets/activities/new_user.html b/ckan/templates/snippets/activities/new_user.html new file mode 100644 index 00000000000..1293f146c7b --- /dev/null +++ b/ckan/templates/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/snippets/activities/removed_tag.html b/ckan/templates/snippets/activities/removed_tag.html new file mode 100644 index 00000000000..00acfa15ff6 --- /dev/null +++ b/ckan/templates/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/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 new file mode 100644 index 00000000000..8ea29f0f16f --- /dev/null +++ b/ckan/templates/snippets/activity_stream.html @@ -0,0 +1,51 @@ +{% macro actor(activity) %} + + {{ h.linked_user(activity.user_id, 0, 30) }} + +{% endmacro %} + +{% macro dataset(activity) %} + + {{ h.dataset_link(activity.data.package) }} + +{% endmacro %} + +{% macro organization(activity) %} +{{ h.organization_link(activity.data.group) }} +{% endmacro %} + +{% macro user(activity) %} + + {{ h.linked_user(activity.object_id, 0, 20) }} + +{% endmacro %} + +{% macro tag(activity) %} + + {{ h.tag_link(activity.data.tag) }} + +{% endmacro %} + +{% macro group(activity) %} + + {{ h.group_link(activity.data.group) }} + +{% endmacro %} + +{% block activity_stream %} +
      + {% for activity in activity_stream %} + {%- snippet "snippets/activities/{}.html".format( + activity.activity_type.replace(' ', '_') + ), activity=activity, ah={ + 'actor': actor, + 'dataset': dataset, + 'organization': organization, + 'user': user, + 'group': group, + 'tag': tag + } + -%} + {% endfor %} +
    +{% endblock %} diff --git a/ckan/templates/user/activity_stream.html b/ckan/templates/user/activity_stream.html index 290d34d5fa6..6b7579c4426 100644 --- a/ckan/templates/user/activity_stream.html +++ b/ckan/templates/user/activity_stream.html @@ -4,7 +4,5 @@ {% block primary_content_inner %}

    {% block page_heading %}{{ _('Activity Stream') }}{% endblock %}

    - {% block activity_stream %} - {{ c.user_activity_stream | safe }} - {% endblock %} + {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} {% endblock %} diff --git a/ckan/templates/user/dashboard.html b/ckan/templates/user/dashboard.html index dd6c346e6ec..0ec849a3b67 100644 --- a/ckan/templates/user/dashboard.html +++ b/ckan/templates/user/dashboard.html @@ -38,9 +38,7 @@

    {% endblock %} {{ _("Activity from items that I'm following") }}

    - {% block activity_stream %} - {{ c.dashboard_activity_stream }} - {% endblock %} + {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} {% endblock %} diff --git a/ckan/tests/legacy/functional/test_package.py b/ckan/tests/legacy/functional/test_package.py index b09a0f55ef9..41df62cbd7b 100644 --- a/ckan/tests/legacy/functional/test_package.py +++ b/ckan/tests/legacy/functional/test_package.py @@ -692,47 +692,6 @@ def test_read_as_admin(self): res = self.app.get(offset, status=200, extra_environ={'REMOTE_USER':'testsysadmin'}) -class TestRevisions(TestPackageBase): - @classmethod - def setup_class(cls): - model.Session.remove() - model.repo.init_db() - cls.name = u'revisiontest1' - - # create pkg - cls.notes = [u'Written by Puccini', u'Written by Rossini', u'Not written at all', u'Written again', u'Written off'] - rev = model.repo.new_revision() - cls.pkg1 = model.Package(name=cls.name) - cls.pkg1.notes = cls.notes[0] - model.Session.add(cls.pkg1) - model.repo.commit_and_remove() - - # edit pkg - for i in range(5)[1:]: - rev = model.repo.new_revision() - pkg1 = model.Package.by_name(cls.name) - pkg1.notes = cls.notes[i] - model.repo.commit_and_remove() - - cls.pkg1 = model.Package.by_name(cls.name) - cls.revision_ids = [rev[0].id for rev in cls.pkg1.all_related_revisions] - # revision ids are newest first - cls.revision_timestamps = [rev[0].timestamp for rev in cls.pkg1.all_related_revisions] - cls.offset = url_for(controller='package', action='history', id=cls.pkg1.name) - - @classmethod - def teardown_class(cls): - model.repo.rebuild_db() - - def test_2_atom_feed(self): - offset = "%s?format=atom" % self.offset - res = self.app.get(offset) - assert '' in res, res - - - class TestResourceListing(TestPackageBase): @classmethod def setup_class(cls): diff --git a/ckan/tests/legacy/functional/test_revision.py b/ckan/tests/legacy/functional/test_revision.py deleted file mode 100644 index af0f653fa1c..00000000000 --- a/ckan/tests/legacy/functional/test_revision.py +++ /dev/null @@ -1,155 +0,0 @@ -# encoding: utf-8 - -from ckan.tests.legacy import TestController, CreateTestData, url_for -import ckan.model as model - -# TODO: purge revisions after creating them -class TestRevisionController(TestController): - - @classmethod - def setup_class(self): - model.Session.remove() - # rebuild db before this test as it depends delicately on what - # revisions exist - model.repo.init_db() - CreateTestData.create() - - @classmethod - def teardown_class(self): - model.repo.rebuild_db() - - def create_40_revisions(self): - for i in range(0,40): - rev = model.repo.new_revision() - rev.author = "Test Revision %s" % i - model.repo.commit() - - def assert_click(self, res, link_exp, res2_exp): - try: - # paginate links are also just numbers - # res2 = res.click('^%s$' % link_exp) - res2 = res.click(link_exp) - except: - print "\nThe first response (list):\n\n" - print str(res) - print "\nThe link that couldn't be followed:" - print str(link_exp) - raise - try: - assert res2_exp in res2 - except: - print "\nThe first response (list):\n\n" - print str(res) - print "\nThe second response (item):\n\n" - print str(res2) - print "\nThe followed link:" - print str(link_exp) - print "\nThe expression that couldn't be found:" - print str(res2_exp) - raise - - def create_updating_revision(self, name, **kwds): - rev = model.repo.new_revision() - rev.author = "Test Revision Updating" - package = self.get_package(name) - if 'resources' in kwds: - resources = kwds.pop('resources') - for resource in package.resources_all: - resource.state = 'deleted' - for resource in resources: - resource = model.Resource(**resource) - model.Session.add(resource) - package.resources_all.append(resource) - if 'extras' in kwds: - extras_data = kwds.pop('extras') - # extras = [] - # for key,value in extras_data.items(): - # extra = model.PackageExtra(key=key, value=value) - # model.Session.add(extra) - # extras.append(extra) - for key,value in extras_data.items(): - package.extras[key] = value - for name,value in kwds.items(): - setattr(package, name, value) - model.Session.add(package) - model.Session.commit() - model.Session.remove() - if not model.repo.history()[0].packages: - raise Exception, "Didn't set up revision right." - - def create_deleting_revision(self, name): - rev = model.repo.new_revision() - rev.author = "Test Revision Deleting" - package = self.get_package(name) - package.delete() - model.repo.commit() - - def get_package(self, name): - return model.Package.by_name(name) - - def test_read(self): - anna = model.Package.by_name(u'annakarenina') - rev_id = anna.revision.id - offset = url_for(controller='revision', action='read', id='%s' % rev_id) - res = self.app.get(offset) - assert 'Revision %s' % rev_id in res - assert 'Revision: %s' % rev_id in res - # Todo: Reinstate asserts below, failing on 'Test Revision Deleting' - #assert 'Author: tester' in res - #assert 'Log Message:' in res - #assert 'Creating test data.' in res - #assert 'Dataset: annakarenina' in res - #assert "Datasets' Tags" in res - #res = res.click('annakarenina', index=0) - #assert 'Datasets - annakarenina' in res - - def test_list_format_atom(self): - self.create_40_revisions() - self.create_updating_revision(u'warandpeace', - title=u"My Updated 'War and Peace' Title", - ) - self.create_updating_revision(u'annakarenina', - title=u"My Updated 'Annakarenina' Title", - resources=[{ - 'url': u'http://datahub.io/download3', - 'format': u'zip file', - 'description': u'Full text. Needs escaping: " Umlaut: \xfc', - 'hash': u'def456', - }], - ) - self.create_updating_revision(u'warandpeace', - title=u"My Doubly Updated 'War and Peace' Title", - extras={ - 'date_updated': u'2010', - } - ) - self.create_deleting_revision(u'annakarenina') - revisions = model.repo.history().all() - revision1 = revisions[0] - # Revisions are most recent first, with first rev on last page. - # Todo: Look at the model to see which revision is last. - # Todo: Test for last revision on first page. - # Todo: Test for first revision on last page. - # Todo: Test for last revision minus 50 on second page. - # Page 1. (Implied id=1) - offset = url_for(controller='revision', action='list', format='atom') - res = self.app.get(offset) - assert '' in res, res - # Todo: Better test for 'days' request param. - # - fake some older revisions and check they aren't included. - offset = url_for(controller='revision', action='list', format='atom', - days=30) - res = self.app.get(offset) - assert '' in res, res - - # Tests for indications about what happened. - assert 'warandpeace:created' in res, res - assert 'annakarenina:created' in res, res - assert 'warandpeace:updated:date_updated' in res, res - assert 'annakarenina:updated:resources' in res, res - assert 'annakarenina:deleted' in res, res - diff --git a/ckan/tests/logic/action/test_get.py b/ckan/tests/logic/action/test_get.py index c3f49f3b0db..c6ec9a9ebab 100644 --- a/ckan/tests/logic/action/test_get.py +++ b/ckan/tests/logic/action/test_get.py @@ -1310,11 +1310,6 @@ def test_activity_list_actions(self): 'group_activity_list', 'organization_activity_list', 'recently_changed_packages_activity_list', - 'user_activity_list_html', - 'package_activity_list_html', - 'group_activity_list_html', - 'organization_activity_list_html', - 'recently_changed_packages_activity_list_html', 'current_package_list_with_resources', ] for action in actions: From 1b6b5de51ae3f3a41a157e19c44920cf190e923b Mon Sep 17 00:00:00 2001 From: David Read Date: Tue, 2 Jan 2018 12:02:21 +0000 Subject: [PATCH 002/103] Add bootstrap2 versions of html files --- .../snippets/activities/added_tag.html | 14 +++++ .../snippets/activities/changed_group.html | 13 +++++ .../activities/changed_organization.html | 13 +++++ .../snippets/activities/changed_package.html | 17 +++++++ .../snippets/activities/changed_resource.html | 14 +++++ .../snippets/activities/changed_user.html | 12 +++++ .../snippets/activities/deleted_group.html | 13 +++++ .../activities/deleted_organization.html | 13 +++++ .../snippets/activities/deleted_package.html | 13 +++++ .../snippets/activities/deleted_resource.html | 14 +++++ .../snippets/activities/follow_dataset.html | 13 +++++ .../snippets/activities/follow_group.html | 13 +++++ .../snippets/activities/follow_user.html | 13 +++++ .../snippets/activities/new_group.html | 13 +++++ .../snippets/activities/new_organization.html | 13 +++++ .../snippets/activities/new_package.html | 17 +++++++ .../snippets/activities/new_resource.html | 14 +++++ .../snippets/activities/new_user.html | 12 +++++ .../snippets/activities/removed_tag.html | 14 +++++ .../snippets/activity_stream.html | 51 +++++++++++++++++++ 20 files changed, 309 insertions(+) create mode 100644 ckan/templates-bs2/snippets/activities/added_tag.html create mode 100644 ckan/templates-bs2/snippets/activities/changed_group.html create mode 100644 ckan/templates-bs2/snippets/activities/changed_organization.html create mode 100644 ckan/templates-bs2/snippets/activities/changed_package.html create mode 100644 ckan/templates-bs2/snippets/activities/changed_resource.html create mode 100644 ckan/templates-bs2/snippets/activities/changed_user.html create mode 100644 ckan/templates-bs2/snippets/activities/deleted_group.html create mode 100644 ckan/templates-bs2/snippets/activities/deleted_organization.html create mode 100644 ckan/templates-bs2/snippets/activities/deleted_package.html create mode 100644 ckan/templates-bs2/snippets/activities/deleted_resource.html create mode 100644 ckan/templates-bs2/snippets/activities/follow_dataset.html create mode 100644 ckan/templates-bs2/snippets/activities/follow_group.html create mode 100644 ckan/templates-bs2/snippets/activities/follow_user.html create mode 100644 ckan/templates-bs2/snippets/activities/new_group.html create mode 100644 ckan/templates-bs2/snippets/activities/new_organization.html create mode 100644 ckan/templates-bs2/snippets/activities/new_package.html create mode 100644 ckan/templates-bs2/snippets/activities/new_resource.html create mode 100644 ckan/templates-bs2/snippets/activities/new_user.html create mode 100644 ckan/templates-bs2/snippets/activities/removed_tag.html create mode 100644 ckan/templates-bs2/snippets/activity_stream.html 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..e78bd8bb952 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/changed_package.html @@ -0,0 +1,17 @@ +
  • + +

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

    +
  • 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/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..fb421dcccf6 --- /dev/null +++ b/ckan/templates-bs2/snippets/activities/new_package.html @@ -0,0 +1,17 @@ +
  • + +

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

    +
  • 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_stream.html b/ckan/templates-bs2/snippets/activity_stream.html new file mode 100644 index 00000000000..8ea29f0f16f --- /dev/null +++ b/ckan/templates-bs2/snippets/activity_stream.html @@ -0,0 +1,51 @@ +{% macro actor(activity) %} + + {{ h.linked_user(activity.user_id, 0, 30) }} + +{% endmacro %} + +{% macro dataset(activity) %} + + {{ h.dataset_link(activity.data.package) }} + +{% endmacro %} + +{% macro organization(activity) %} +{{ h.organization_link(activity.data.group) }} +{% endmacro %} + +{% macro user(activity) %} + + {{ h.linked_user(activity.object_id, 0, 20) }} + +{% endmacro %} + +{% macro tag(activity) %} + + {{ h.tag_link(activity.data.tag) }} + +{% endmacro %} + +{% macro group(activity) %} + + {{ h.group_link(activity.data.group) }} + +{% endmacro %} + +{% block activity_stream %} +
      + {% for activity in activity_stream %} + {%- snippet "snippets/activities/{}.html".format( + activity.activity_type.replace(' ', '_') + ), activity=activity, ah={ + 'actor': actor, + 'dataset': dataset, + 'organization': organization, + 'user': user, + 'group': group, + 'tag': tag + } + -%} + {% endfor %} +
    +{% endblock %} From 2a8f384f6f9c16f53f2c3770004d9e8de1eb4426 Mon Sep 17 00:00:00 2001 From: David Read Date: Tue, 2 Jan 2018 16:05:19 +0000 Subject: [PATCH 003/103] [3972] Minor reformatting niggles. --- ckan/controllers/group.py | 7 +------ ckan/controllers/package.py | 7 +------ ckan/controllers/user.py | 20 +++----------------- ckan/logic/action/get.py | 7 +++---- ckan/model/package.py | 1 - 5 files changed, 8 insertions(+), 34 deletions(-) diff --git a/ckan/controllers/group.py b/ckan/controllers/group.py index 2ce6e7d0b51..dd745f976a2 100644 --- a/ckan/controllers/group.py +++ b/ckan/controllers/group.py @@ -853,12 +853,7 @@ def activity(self, id, offset=0): extra_vars={ 'group_type': group_type, 'activity_stream': get_action(activity_action)( - context, - { - 'id': c.group_dict['id'], - 'offset': offset - } - ) + context, {'id': c.group_dict['id'], 'offset': offset}) } ) diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 41476149d91..0d99dfe307f 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -1224,12 +1224,7 @@ def activity(self, id): extra_vars={ 'dataset_type': dataset_type, 'activity_stream': get_action('package_activity_list')( - context, - { - 'id': id - } - ) - } + context, {'id': id})} ) def resource_embedded_dataviewer(self, id, resource_id, diff --git a/ckan/controllers/user.py b/ckan/controllers/user.py index bb4d981d3df..d2913b5a7bd 100644 --- a/ckan/controllers/user.py +++ b/ckan/controllers/user.py @@ -595,13 +595,7 @@ def activity(self, id, offset=0): 'user/activity_stream.html', extra_vars={ 'activity_stream': get_action('user_activity_list')( - context, - { - 'id': id, - 'offset': offset - } - ) - } + context, {'id': id, 'offset': offset})} ) def _get_dashboard_context(self, filter_type=None, filter_id=None, q=None): @@ -669,18 +663,10 @@ def dashboard(self, id=None, offset=0): filter_id = request.params.get('name', u'') c.followee_list = get_action('followee_list')( - context, - { - 'id': c.userobj.id, - 'q': q - } - ) + context, {'id': c.userobj.id, 'q': q}) c.dashboard_activity_stream_context = self._get_dashboard_context( - filter_type, - filter_id, - q - ) + filter_type, filter_id, q) dashboard_activity_stream = h.dashboard_activity_stream( c.userobj.id, filter_type, filter_id, offset ) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 8ca08f058ed..d66c156c433 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2660,10 +2660,9 @@ def organization_activity_list(context, data_dict): activity_objects = model.activity.group_activity_list(org_id, limit=limit, offset=offset) - """ - activity_objects = _filter_activity_by_user(_activity_objects, - _activity_stream_get_filtered_users()) - """ + # WHAT? + # activity_objects = _filter_activity_by_user(_activity_objects, + # _activity_stream_get_filtered_users()) return model_dictize.activity_list_dictize(activity_objects, context) diff --git a/ckan/model/package.py b/ckan/model/package.py index a67306e975c..0cf14423447 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -558,7 +558,6 @@ def activity_stream_item(self, activity_type, revision, user_id): '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. - # Legacy tests do not include valid users :( 'actor': actor.name if actor else None } ) From 0bdf262651207f1818348e7474d356b2eecda8fc Mon Sep 17 00:00:00 2001 From: David Read Date: Tue, 2 Jan 2018 18:01:53 +0000 Subject: [PATCH 004/103] Update less version, to avoid merge main.css conflicts in future. Clearly the last person to use less used a more recent version (than 2.5.2) because the color values in main.css are no longer long-hand (e.g. main.css has #ffffff rather than the #fff short-hand found in the .less file) --- doc/contributing/frontend/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/contributing/frontend/index.rst b/doc/contributing/frontend/index.rst index 888e8061e5c..6bbe193c1fc 100644 --- a/doc/contributing/frontend/index.rst +++ b/doc/contributing/frontend/index.rst @@ -49,7 +49,7 @@ style script. :: - $ npm install less@1.7.5 nodewatch + $ npm install less@2.7.2 nodewatch You may need to use ``sudo`` depending on your CKAN install type. From 626a88fa5cf81146581b2ab373589971b6d8fee7 Mon Sep 17 00:00:00 2001 From: David Read Date: Tue, 2 Jan 2018 18:08:37 +0000 Subject: [PATCH 005/103] Resource links and resource page now take the activity_id param so you can look at old versions of those too. --- ckan/controllers/package.py | 38 ++++++++++++++----- ckan/public/base/css/main.css | 3 ++ ckan/public/base/less/dataset.less | 6 ++- ckan/templates-bs2/package/read.html | 2 +- ckan/templates-bs2/package/resource_read.html | 14 ++++++- ckan/templates-bs2/package/resources.html | 2 +- .../package/snippets/resource_item.html | 2 +- .../package/snippets/resources.html | 2 +- .../package/snippets/resources_list.html | 4 +- ckan/templates/package/read.html | 2 +- ckan/templates/package/resource_read.html | 14 ++++++- ckan/templates/package/resources.html | 2 +- .../package/snippets/resource_item.html | 2 +- .../templates/package/snippets/resources.html | 2 +- .../package/snippets/resources_list.html | 4 +- 15 files changed, 73 insertions(+), 26 deletions(-) diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 0d99dfe307f..a69207f3b51 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -358,21 +358,27 @@ def read(self, id): data_dict = {'id': id, 'include_tracking': True} activity_id = request.params.get('activity_id') - # check if package exists + # get the package dict try: c.pkg_dict = get_action('package_show')(context, data_dict) c.pkg = context['package'] - - if activity_id: + # NB templates should not use c.pkg, because it takes no account + # of activity_id (among other reasons) + except (NotFound, NotAuthorized): + abort(404, _('Dataset not found')) + if activity_id: + # view an 'old' version of the package, as recorded in the + # activity stream + try: c.pkg_dict = context['session'].query(model.Activity).get( activity_id ).data['package'] - # Don't crash on old activity records, which do not include - # resources or extras. - c.pkg_dict.setdefault('resources', []) - c.is_activity_archive = True - except (NotFound, NotAuthorized): - abort(404, _('Dataset not found')) + except AttributeError: + abort(404, _('Dataset not found')) + # Don't crash on old activity records, which do not include + # resources or extras. + c.pkg_dict.setdefault('resources', []) + c.is_activity_archive = True # used by disqus plugin c.current_package_id = c.pkg.id @@ -993,6 +999,20 @@ def resource_read(self, id, resource_id): c.package = get_action('package_show')(context, {'id': id}) except (NotFound, NotAuthorized): abort(404, _('Dataset not found')) + activity_id = request.params.get('activity_id') + if activity_id: + # view an 'old' version of the package, as recorded in the + # activity stream + try: + c.package = context['session'].query(model.Activity).get( + activity_id + ).data['package'] + except AttributeError: + abort(404, _('Dataset not found')) + # Don't crash on old activity records, which do not include + # resources or extras. + c.package.setdefault('resources', []) + c.is_activity_archive = True for resource in c.package.get('resources', []): if resource['id'] == resource_id: diff --git a/ckan/public/base/css/main.css b/ckan/public/base/css/main.css index 45a75e4b1b9..9bcccd554a2 100644 --- a/ckan/public/base/css/main.css +++ b/ckan/public/base/css/main.css @@ -8232,6 +8232,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 8076a0f47b1..0216f58808e 100644 --- a/ckan/public/base/less/dataset.less +++ b/ckan/public/base/less/dataset.less @@ -337,4 +337,8 @@ .resource-view { margin-top: 20px; -} \ No newline at end of file +} + +#activity-archive-notice { + clear: both; +} diff --git a/ckan/templates-bs2/package/read.html b/ckan/templates-bs2/package/read.html index 3d7e48b7b2c..3b05a2d0f0f 100644 --- a/ckan/templates-bs2/package/read.html +++ b/ckan/templates-bs2/package/read.html @@ -13,7 +13,7 @@ {% endif %} {% block package_archive_notice %} {% if c.is_activity_archive %} -
    +
    {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} You're currently viewing an old version of this dataset. Some resources may no longer exist or the dataset may not display correctly. To see the diff --git a/ckan/templates-bs2/package/resource_read.html b/ckan/templates-bs2/package/resource_read.html index 8e51e1862eb..c163ea0388f 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 c.is_activity_archive %}
    • {% link_for _('Manage'), controller='package', action='resource_edit', id=pkg.name, resource_id=res.id, class_='btn', icon='wrench' %}
    • {% endif %} {% if res.url and h.is_url(res.url) %} @@ -76,6 +76,17 @@ {% endblock %}
    {% block resource_content %} + {% block package_archive_notice %} + {% if c.is_activity_archive %} +
    + {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} + You're currently viewing an old version of this dataset. Some resources + may no longer exist or the dataset may not display correctly. 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) %} @@ -218,4 +229,3 @@

    {{ _('Additional Information') }}

    {% snippet "snippets/social.html" %} {% endblock %} {% endblock %} - diff --git a/ckan/templates-bs2/package/resources.html b/ckan/templates-bs2/package/resources.html index dcb708e6d40..19af7721382 100644 --- a/ckan/templates-bs2/package/resources.html +++ b/ckan/templates-bs2/package/resources.html @@ -11,7 +11,7 @@ {% block primary_content_inner %} {% if pkg.resources %}
      - {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} + {% set can_edit = h.check_access('package_update', {'id':pkg.id }) and not c.is_activity_archive %} {% for resource in pkg.resources %} {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit %} {% endfor %} diff --git a/ckan/templates-bs2/package/snippets/resource_item.html b/ckan/templates-bs2/package/snippets/resource_item.html index 63541c2cc64..d93cc6b5e47 100644 --- a/ckan/templates-bs2/package/snippets/resource_item.html +++ b/ckan/templates-bs2/package/snippets/resource_item.html @@ -1,5 +1,5 @@ {% set url_action = 'resource_edit' if url_is_edit and can_edit else 'resource_read' %} -{% set url = h.url_for(controller='package', action=url_action, id=pkg.name, resource_id=res.id) %} +{% set url = h.url_for(controller='package', action=url_action, id=pkg.name, resource_id=res.id, **({'activity_id': request.params['activity_id']} if 'activity_id' in request.params else {}))) %}
    • {% block resource_item_title %} diff --git a/ckan/templates-bs2/package/snippets/resources.html b/ckan/templates-bs2/package/snippets/resources.html index fe1d1f3c9d0..26c5a1c67a4 100644 --- a/ckan/templates-bs2/package/snippets/resources.html +++ b/ckan/templates-bs2/package/snippets/resources.html @@ -23,7 +23,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 140df8fb627..625037afd05 100644 --- a/ckan/templates-bs2/package/snippets/resources_list.html +++ b/ckan/templates-bs2/package/snippets/resources_list.html @@ -1,4 +1,4 @@ -{# +{# Renders a list of resources with icons and view links. resources - A list of resources to render @@ -15,7 +15,7 @@

    {{ _('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 c.is_activity_archive %} {% for resource in resources %} {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit %} {% endfor %} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 3d7e48b7b2c..3b05a2d0f0f 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -13,7 +13,7 @@ {% endif %} {% block package_archive_notice %} {% if c.is_activity_archive %} -
      +
      {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} You're currently viewing an old version of this dataset. Some resources may no longer exist or the dataset may not display correctly. To see the diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index 6e6a812172c..f17a0f52606 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 c.is_activity_archive %}
      • {% link_for _('Manage'), controller='package', action='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) %} @@ -70,6 +70,17 @@ {% endblock %}
      {% block resource_content %} + {% block package_archive_notice %} + {% if c.is_activity_archive %} +
      + {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} + You're currently viewing an old version of this dataset. Some resources + may no longer exist or the dataset may not display correctly. 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) %} @@ -210,4 +221,3 @@

      {{ _('Additional Information') }}

      {% snippet "snippets/social.html" %} {% endblock %} {% endblock %} - diff --git a/ckan/templates/package/resources.html b/ckan/templates/package/resources.html index dcb708e6d40..19af7721382 100644 --- a/ckan/templates/package/resources.html +++ b/ckan/templates/package/resources.html @@ -11,7 +11,7 @@ {% block primary_content_inner %} {% if pkg.resources %}
        - {% set can_edit = h.check_access('package_update', {'id':pkg.id }) %} + {% set can_edit = h.check_access('package_update', {'id':pkg.id }) and not c.is_activity_archive %} {% for resource in pkg.resources %} {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, url_is_edit=true, can_edit=can_edit %} {% endfor %} diff --git a/ckan/templates/package/snippets/resource_item.html b/ckan/templates/package/snippets/resource_item.html index 992397e10f4..006498afff7 100644 --- a/ckan/templates/package/snippets/resource_item.html +++ b/ckan/templates/package/snippets/resource_item.html @@ -1,5 +1,5 @@ {% set url_action = 'resource_edit' if url_is_edit and can_edit else 'resource_read' %} -{% set url = h.url_for(controller='package', action=url_action, id=pkg.name, resource_id=res.id) %} +{% set url = h.url_for(controller='package', action=url_action, id=pkg.name, resource_id=res.id, **({'activity_id': request.params['activity_id']} if 'activity_id' in request.params else {})) %}
      • {% block resource_item_title %} diff --git a/ckan/templates/package/snippets/resources.html b/ckan/templates/package/snippets/resources.html index cf7ed3fbef0..eac6e8ce7a3 100644 --- a/ckan/templates/package/snippets/resources.html +++ b/ckan/templates/package/snippets/resources.html @@ -23,7 +23,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 140df8fb627..625037afd05 100644 --- a/ckan/templates/package/snippets/resources_list.html +++ b/ckan/templates/package/snippets/resources_list.html @@ -1,4 +1,4 @@ -{# +{# Renders a list of resources with icons and view links. resources - A list of resources to render @@ -15,7 +15,7 @@

      {{ _('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 c.is_activity_archive %} {% for resource in resources %} {% snippet 'package/snippets/resource_item.html', pkg=pkg, res=resource, can_edit=can_edit %} {% endfor %} From 317e63f66bfa5839fbe907829154bfedd072314e Mon Sep 17 00:00:00 2001 From: David Read Date: Mon, 5 Feb 2018 21:40:13 +0000 Subject: [PATCH 006/103] Trying using logic layer activity_show. Playing about with permissions. --- ckan/controllers/package.py | 19 +++-- ckan/lib/dictization/model_dictize.py | 10 ++- ckan/logic/action/get.py | 79 +++++++++++++++++-- ckan/logic/auth/__init__.py | 2 +- ckan/logic/auth/get.py | 19 +++++ ckan/logic/schema.py | 4 +- ckan/templates-bs2/package/read.html | 3 +- ckan/templates-bs2/package/resource_read.html | 3 +- ckan/templates/package/activity.html | 2 +- ckan/templates/package/read.html | 3 +- ckan/templates/package/resource_read.html | 3 +- ckan/templates/snippets/activity_stream.html | 11 ++- 12 files changed, 127 insertions(+), 31 deletions(-) diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index a69207f3b51..b10706a36e3 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -370,13 +370,19 @@ def read(self, id): # view an 'old' version of the package, as recorded in the # activity stream try: - c.pkg_dict = context['session'].query(model.Activity).get( - activity_id - ).data['package'] - except AttributeError: + activity = get_action('activity_show')(context, + {'id': activity_id}) + except NotFound: + abort(404, _('Activity not found')) + except NotAuthorized: + abort(403, _('Unauthorized to create a package')) + try: + c.pkg_dict = activity['data']['package'] + except KeyError: abort(404, _('Dataset not found')) - # Don't crash on old activity records, which do not include - # resources or extras. + # Earlier versions of CKAN only stored the package table in the + # activity, so add a placeholder for resources, or the template + # will crash. c.pkg_dict.setdefault('resources', []) c.is_activity_archive = True @@ -740,7 +746,6 @@ def read_ajax(self, id, revision=None): return h.json.dumps(data) def history_ajax(self, id): - context = {'model': model, 'session': model.Session, 'user': c.user, 'auth_user_obj': c.userobj} data_dict = {'id': id} diff --git a/ckan/lib/dictization/model_dictize.py b/ckan/lib/dictization/model_dictize.py index f39c9c3057a..1fd64fe4b0e 100644 --- a/ckan/lib/dictization/model_dictize.py +++ b/ckan/lib/dictization/model_dictize.py @@ -695,12 +695,15 @@ 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: + del activity_dict['data'] return activity_dict -def activity_list_dictize(activity_list, context): - return [activity_dictize(activity, context) for activity in activity_list] +def activity_list_dictize(activity_list, context, include_data=False): + return [activity_dictize(activity, context, include_data) + for activity in activity_list] def activity_detail_dictize(activity_detail, context): return d.table_dictize(activity_detail, context) @@ -764,4 +767,3 @@ def resource_view_list_dictize(resource_views, context): for view in resource_views: resource_view_dicts.append(resource_view_dictize(view, context)) return resource_view_dicts - diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index d66c156c433..d977b3e6192 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -1002,7 +1002,7 @@ def package_show(context, data_dict): package_dict_validated = False metadata_modified = pkg.metadata_modified.isoformat() search_metadata_modified = search_result['metadata_modified'] - # solr stores less precice datetime, + # solr stores less precise datetime, # truncate to 22 charactors to get good enough match if metadata_modified[:22] != search_metadata_modified[:22]: package_dict = None @@ -2572,7 +2572,9 @@ def package_activity_list(context, data_dict): ''' # FIXME: Filter out activities whose subject or object the user is not # authorized to read. - _check_access('package_show', context, data_dict) + data_dict['object_type'] = 'package' + data_dict['include_data'] = False + _check_access('activity_show', context, data_dict) model = context['model'] @@ -2590,7 +2592,8 @@ def package_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) @@ -2700,16 +2703,51 @@ def recently_changed_packages_activity_list(context, data_dict): def activity_detail_list(context, data_dict): '''Return an activity's list of activity detail items. - :param id: the id of the activity + :param id: the id or name of the object (e.g. dataset) + (or activity - DEPRECATED) :type id: string - :rtype: list of dictionaries. + :param object_type: the type of the object being identified. + Accepted values: ``'package'``, ``'activity'`` (deprecated, default) + (``'activity'`` is just there for backward compatibility) + :type object_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) + object_type = data_dict.get('object_type', 'activity') + allowed_object_types = ('package', 'activity') + if object_type not in allowed_object_types: + raise logic.ValidationError('object_type not accepted. Choose from: {}' + .format(allowed_object_types)) + name_or_id = data_dict.get("id") or _get_or_bust(data_dict, 'name_or_id') + if object_type != 'activity': + # i.e. object_type == 'package': + # context['package'] = model.Package.get(activity.object_id) + _check_access('activity_show', context, data_dict) + + offset = int(data_dict.get('offset', 0)) + limit = int( + data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) + + _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()) + + return model_dictize.activity_list_dictize(activity_objects, context) + # activity_detail_objects = \ + # model.Session.query(model.ActivityDetail) \ + # .filter_by(object_type=object_type) \ + # .filter_by(object_id=id) \ + # .join(model.Activity) \ + # .order_by(model.Activity.timestamp) + else: + activity_detail_objects = \ + model.ActivityDetail.by_activity_id(activity_id) + if 'package' in activity.activity_type: + pass + _check_access(u'activity_detail_show', context, data_dict) return model_dictize.activity_detail_list_dictize( activity_detail_objects, context) @@ -3261,6 +3299,31 @@ 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 + + :rtype: dictionary + ''' + model = context['model'] + user = context['user'] + activity_id = _get_or_bust(data_dict, 'id') + + activity = model.Session.query(model.Activity).get(activity_id) + if activity is None: + raise NotFound + context['activity'] = activity + + if 'package' in activity.activity_type: + context['package'] = model.Package.get(activity.object_id) + + _check_access(u'activity_show', context, data_dict) + + activity = model_dictize.activity_dictize(activity, context) + return activity + 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 b340aadcec2..fc0d39b511f 100644 --- a/ckan/logic/auth/__init__.py +++ b/ckan/logic/auth/__init__.py @@ -8,7 +8,7 @@ def _get_object(context, data_dict, name, class_name): - # return the named item if in the data_dict, or get it from + # return the named item if in the context, or get it from # model.class_name try: return context[name] diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index eb78f947a86..1aaccd71d8c 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -260,6 +260,25 @@ def dashboard_new_activities_count(context, data_dict): context, data_dict) +def activity_show(context, data_dict): + if data_dict.get('include_data'): + # The 'data' field of the activity is restricted to users who are + # allowed to edit the object + if data_dict.get('object_type') == 'package': + return {'success': authz.is_authorized_boolean( + 'package_update', context, {'id': data_dict['id']})} + else: + raise {'success': False, 'msg': 'object_type not recognized'} + + # the activity for an object (i.e. the activity metadata) can be seen if + # the user can see the object + if data_dict.get('object_type') == 'package': + return authz.is_authorized('package_show', context, + {'id': data_dict['id']}) + else: + raise {'success': False, 'msg': 'object_type not recognized'} + + 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 88d2cbda2e3..4189de084c6 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -607,9 +607,11 @@ def default_dashboard_activity_list_schema(unicode_safe): @validator_args -def default_activity_list_schema(not_missing, unicode_safe): +def default_activity_list_schema( + ignore_missing, not_missing, unicode_safe, boolean_validator): schema = default_pagination_schema() schema['id'] = [not_missing, unicode_safe] + schema['include_data'] = [ignore_missing, boolean_validator] return schema diff --git a/ckan/templates-bs2/package/read.html b/ckan/templates-bs2/package/read.html index 3b05a2d0f0f..936fca645d0 100644 --- a/ckan/templates-bs2/package/read.html +++ b/ckan/templates-bs2/package/read.html @@ -15,8 +15,7 @@ {% if c.is_activity_archive %}
        {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} - You're currently viewing an old version of this dataset. Some resources - may no longer exist or the dataset may not display correctly. To see the + You're currently viewing an old version of this dataset. To see the current version, click here. {% endtrans %}
        diff --git a/ckan/templates-bs2/package/resource_read.html b/ckan/templates-bs2/package/resource_read.html index c163ea0388f..aac56d22062 100644 --- a/ckan/templates-bs2/package/resource_read.html +++ b/ckan/templates-bs2/package/resource_read.html @@ -80,8 +80,7 @@ {% if c.is_activity_archive %}
        {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} - You're currently viewing an old version of this dataset. Some resources - may no longer exist or the dataset may not display correctly. To see the + You're currently viewing an old version of this dataset. To see the current version, click here. {% endtrans %}
        diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index b54cf190c04..b6c850adbf7 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -8,5 +8,5 @@

        {{ _('Activity Stream') }} {% endblock %}

        - {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream %} + {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream, id=id %} {% endblock %} diff --git a/ckan/templates/package/read.html b/ckan/templates/package/read.html index 3b05a2d0f0f..936fca645d0 100644 --- a/ckan/templates/package/read.html +++ b/ckan/templates/package/read.html @@ -15,8 +15,7 @@ {% if c.is_activity_archive %}
        {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} - You're currently viewing an old version of this dataset. Some resources - may no longer exist or the dataset may not display correctly. To see the + You're currently viewing an old version of this dataset. To see the current version, click here. {% endtrans %}
        diff --git a/ckan/templates/package/resource_read.html b/ckan/templates/package/resource_read.html index f17a0f52606..ec02e9b173b 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -74,8 +74,7 @@ {% if c.is_activity_archive %}
        {% trans url=h.url_for(controller='package', action='read', id=pkg.id) %} - You're currently viewing an old version of this dataset. Some resources - may no longer exist or the dataset may not display correctly. To see the + You're currently viewing an old version of this dataset. To see the current version, click here. {% endtrans %}
        diff --git a/ckan/templates/snippets/activity_stream.html b/ckan/templates/snippets/activity_stream.html index 8ea29f0f16f..036e18ecc68 100644 --- a/ckan/templates/snippets/activity_stream.html +++ b/ckan/templates/snippets/activity_stream.html @@ -6,7 +6,9 @@ {% macro dataset(activity) %} - {{ h.dataset_link(activity.data.package) }} + {% if 'data' in activity %} + {{ h.dataset_link(activity.data.package) }} + {% endif %} {% endmacro %} @@ -32,8 +34,15 @@ {% endmacro %} +{# Displays an activity stream + +activity_stream - the activity data. e.g. the output from package_activity_list +id - the id of the object (e.g. package) + +#} {% block activity_stream %}
          + {% set can_show_activity_detail = h.check_access('activity_show', {'id': id}) %} {% for activity in activity_stream %} {%- snippet "snippets/activities/{}.html".format( activity.activity_type.replace(' ', '_') From 13f6e15534614777495b0a603a290b48f9cbe03c Mon Sep 17 00:00:00 2001 From: David Read Date: Mon, 12 Mar 2018 21:59:17 +0000 Subject: [PATCH 007/103] Auth working for package activity stream. Auth logic split up into 2 functions: activity_show (which takes activity IDs) and activity_list_show (which takes object IDs). --- ckan/controllers/package.py | 12 +++-- ckan/logic/action/get.py | 18 ++++--- ckan/logic/auth/__init__.py | 4 ++ ckan/logic/auth/get.py | 53 ++++++++++++++----- ckan/model/activity.py | 10 ++++ .../snippets/activities/changed_package.html | 10 ++-- .../snippets/activities/new_package.html | 10 ++-- ckan/templates/package/activity.html | 2 +- .../snippets/activities/changed_package.html | 10 ++-- .../snippets/activities/new_package.html | 10 ++-- ckan/templates/snippets/activity_stream.html | 4 +- 11 files changed, 99 insertions(+), 44 deletions(-) diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index b10706a36e3..a326ab291de 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -370,8 +370,8 @@ def read(self, id): # view an 'old' version of the package, as recorded in the # activity stream try: - activity = get_action('activity_show')(context, - {'id': activity_id}) + activity = get_action('activity_show')( + context, {'id': activity_id, 'include_data': True}) except NotFound: abort(404, _('Activity not found')) except NotAuthorized: @@ -1238,6 +1238,9 @@ def activity(self, id): data_dict = {'id': id} try: c.pkg_dict = get_action('package_show')(context, data_dict) + c.package_activity_stream = get_action( + 'package_activity_list')( + context, {'id': id}) dataset_type = c.pkg_dict['type'] or 'dataset' except NotFound: abort(404, _('Dataset not found')) @@ -1248,8 +1251,9 @@ def activity(self, id): 'package/activity.html', extra_vars={ 'dataset_type': dataset_type, - 'activity_stream': get_action('package_activity_list')( - context, {'id': id})} + 'activity_stream': c.package_activity_stream, + 'id': id, + } ) def resource_embedded_dataviewer(self, id, resource_id, diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index d977b3e6192..0955bd4f0db 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2553,7 +2553,7 @@ def user_activity_list(context, data_dict): @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. @@ -2574,7 +2574,7 @@ def package_activity_list(context, data_dict): # authorized to read. data_dict['object_type'] = 'package' data_dict['include_data'] = False - _check_access('activity_show', context, data_dict) + _check_access('activity_list_show', context, data_dict) model = context['model'] @@ -2723,8 +2723,8 @@ def activity_detail_list(context, data_dict): name_or_id = data_dict.get("id") or _get_or_bust(data_dict, 'name_or_id') if object_type != 'activity': # i.e. object_type == 'package': - # context['package'] = model.Package.get(activity.object_id) - _check_access('activity_show', context, data_dict) + data_dict['include_data'] = True + _check_access('activity_list_show', context, data_dict) offset = int(data_dict.get('offset', 0)) limit = int( @@ -3304,26 +3304,28 @@ def activity_show(context, data_dict): :param id: the id of the activity :type id: string + :param include_data: include the data field (or just the activity metadata) + :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 - if 'package' in activity.activity_type: - context['package'] = model.Package.get(activity.object_id) - _check_access(u'activity_show', context, data_dict) - activity = model_dictize.activity_dictize(activity, context) + activity = model_dictize.activity_dictize(activity, context, + include_data=include_data) return activity + 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 fc0d39b511f..9901c055ae5 100644 --- a/ckan/logic/auth/__init__.py +++ b/ckan/logic/auth/__init__.py @@ -42,3 +42,7 @@ def get_group_object(context, data_dict=None): 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') diff --git a/ckan/logic/auth/get.py b/ckan/logic/auth/get.py index 1aaccd71d8c..764fe3bc4a6 100644 --- a/ckan/logic/auth/get.py +++ b/ckan/logic/auth/get.py @@ -4,7 +4,7 @@ import ckan.authz as authz from ckan.common import _ from ckan.logic.auth import (get_package_object, get_group_object, - get_resource_object) + get_resource_object, get_activity_object) from ckan.lib.plugins import get_permission_labels @@ -260,23 +260,50 @@ def dashboard_new_activities_count(context, data_dict): context, data_dict) -def activity_show(context, data_dict): +def activity_list_show(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') + :type object_type: string + :param include_data: include the data field (or just the activity metadata) + :type include_data: boolean + ''' if data_dict.get('include_data'): # The 'data' field of the activity is restricted to users who are # allowed to edit the object - if data_dict.get('object_type') == 'package': - return {'success': authz.is_authorized_boolean( - 'package_update', context, {'id': data_dict['id']})} - else: - raise {'success': False, 'msg': 'object_type not recognized'} - - # the activity for an object (i.e. the activity metadata) can be seen if - # the user can see the object - if data_dict.get('object_type') == 'package': - return authz.is_authorized('package_show', context, + action_on_which_to_base_auth = 'package_update' + else: + # the activity for an object (i.e. the activity metadata) can be viewed + # if the user can see the object + action_on_which_to_base_auth = 'package_show' + + if data_dict['object_type'] == 'package': + return authz.is_authorized(action_on_which_to_base_auth, context, {'id': data_dict['id']}) else: - raise {'success': False, 'msg': 'object_type not recognized'} + return {'success': False, 'msg': 'object_type not recognized'} + + +def activity_show(context, data_dict): + ''' + :param id: the id of the activity + :type id: string + :param include_data: include the data field (or just the activity metadata) + :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_show(context, { + 'id': activity.object_id, + 'object_type': object_type, + 'include_data': data_dict['include_data'], + }) def user_follower_list(context, data_dict): diff --git a/ckan/model/activity.py b/ckan/model/activity.py index b56d91ecc26..9f8f05f8e92 100644 --- a/ckan/model/activity.py +++ b/ckan/model/activity.py @@ -45,6 +45,7 @@ Column('data', _types.JsonDictType), ) + class Activity(domain_object.DomainObject): def __init__(self, user_id, object_id, revision_id, activity_type, @@ -60,6 +61,15 @@ def __init__(self, user_id, object_id, revision_id, activity_type, 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) diff --git a/ckan/templates-bs2/snippets/activities/changed_package.html b/ckan/templates-bs2/snippets/activities/changed_package.html index e78bd8bb952..ea7fe214344 100644 --- a/ckan/templates-bs2/snippets/activities/changed_package.html +++ b/ckan/templates-bs2/snippets/activities/changed_package.html @@ -8,10 +8,12 @@
          {{ h.time_ago_from_timestamp(activity.timestamp) }} -  |  - - {{ _('View this version') }} - + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %}

          diff --git a/ckan/templates-bs2/snippets/activities/new_package.html b/ckan/templates-bs2/snippets/activities/new_package.html index fb421dcccf6..e257a4a91f3 100644 --- a/ckan/templates-bs2/snippets/activities/new_package.html +++ b/ckan/templates-bs2/snippets/activities/new_package.html @@ -8,10 +8,12 @@
          {{ h.time_ago_from_timestamp(activity.timestamp) }} -  |  - - {{ _('View this version') }} - + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %}

          diff --git a/ckan/templates/package/activity.html b/ckan/templates/package/activity.html index b6c850adbf7..057b17fec6d 100644 --- a/ckan/templates/package/activity.html +++ b/ckan/templates/package/activity.html @@ -8,5 +8,5 @@

          {{ _('Activity Stream') }} {% endblock %}

          - {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream, id=id %} + {% snippet 'snippets/activity_stream.html', activity_stream=activity_stream, id=id, object_type='package' %} {% endblock %} diff --git a/ckan/templates/snippets/activities/changed_package.html b/ckan/templates/snippets/activities/changed_package.html index e78bd8bb952..ea7fe214344 100644 --- a/ckan/templates/snippets/activities/changed_package.html +++ b/ckan/templates/snippets/activities/changed_package.html @@ -8,10 +8,12 @@
          {{ h.time_ago_from_timestamp(activity.timestamp) }} -  |  - - {{ _('View this version') }} - + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %}

          diff --git a/ckan/templates/snippets/activities/new_package.html b/ckan/templates/snippets/activities/new_package.html index fb421dcccf6..e257a4a91f3 100644 --- a/ckan/templates/snippets/activities/new_package.html +++ b/ckan/templates/snippets/activities/new_package.html @@ -8,10 +8,12 @@
          {{ h.time_ago_from_timestamp(activity.timestamp) }} -  |  - - {{ _('View this version') }} - + {% if can_show_activity_detail %} +  |  + + {{ _('View this version') }} + + {% endif %}

          diff --git a/ckan/templates/snippets/activity_stream.html b/ckan/templates/snippets/activity_stream.html index 036e18ecc68..e87965e2373 100644 --- a/ckan/templates/snippets/activity_stream.html +++ b/ckan/templates/snippets/activity_stream.html @@ -42,11 +42,11 @@ #} {% block activity_stream %}
            - {% set can_show_activity_detail = h.check_access('activity_show', {'id': id}) %} + {% set can_show_activity_detail = h.check_access('activity_list_show', {'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={ + ), activity=activity, can_show_activity_detail=can_show_activity_detail, ah={ 'actor': actor, 'dataset': dataset, 'organization': organization, From 0ec1f727483d10c1c2cab708799ad656e460fed9 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 16 Mar 2018 11:29:10 +0000 Subject: [PATCH 008/103] Put back the hiding of actions by sysadmin (command-line things) --- ckan/logic/action/get.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 0955bd4f0db..833fc7dc458 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2661,11 +2661,10 @@ def organization_activity_list(context, data_dict): org_show = logic.get_action('organization_show') org_id = org_show(context, {'id': org_id})['id'] - activity_objects = model.activity.group_activity_list(org_id, + _activity_objects = model.activity.group_activity_list(org_id, limit=limit, offset=offset) - # WHAT? - # activity_objects = _filter_activity_by_user(_activity_objects, - # _activity_stream_get_filtered_users()) + activity_objects = _filter_activity_by_user(_activity_objects, + _activity_stream_get_filtered_users()) return model_dictize.activity_list_dictize(activity_objects, context) From 88e1202f29e638280c142a457f864b582a513901 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 16 Mar 2018 11:55:16 +0000 Subject: [PATCH 009/103] Add action func to get an old version of a package, making it easy to diff on the command-line. --- ckan/logic/action/get.py | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 833fc7dc458..59dd9455b0b 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -3325,6 +3325,46 @@ def activity_show(context, data_dict): return activity +def activity_data_show(context, data_dict): + '''Show the data from an item of 'activity' (part of the activity stream). + + For example you can get just the old version of a dataset, without 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') + data_dict['include_data'] = True + + 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=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 _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 From d0e0ebff5c79b914a346d9a0f60e95239ed8b4bb Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 16 Mar 2018 12:35:08 +0000 Subject: [PATCH 010/103] No longer record ActivityDetail - it is now deprecated --- ckan/controllers/package.py | 1 + .../lib/activity_streams_session_extension.py | 29 ------------------- ckan/model/package.py | 15 ---------- ckan/model/package_extra.py | 17 +---------- ckan/model/resource.py | 17 ----------- ckan/model/tag.py | 26 ----------------- 6 files changed, 2 insertions(+), 103 deletions(-) diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index a326ab291de..7aff4a19ac1 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -746,6 +746,7 @@ def read_ajax(self, id, revision=None): return h.json.dumps(data) def history_ajax(self, id): + context = {'model': model, 'session': model.Session, 'user': c.user, 'auth_user_obj': c.userobj} data_dict = {'id': id} diff --git a/ckan/lib/activity_streams_session_extension.py b/ckan/lib/activity_streams_session_extension.py index 85cf677f948..b0166605e64 100644 --- a/ckan/lib/activity_streams_session_extension.py +++ b/ckan/lib/activity_streams_session_extension.py @@ -18,15 +18,6 @@ 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. @@ -89,10 +80,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] @@ -130,24 +117,8 @@ def before_commit(self, session): 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] - 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/model/package.py b/ckan/model/package.py index 0cf14423447..4de6c75be71 100644 --- a/ckan/model/package.py +++ b/ckan/model/package.py @@ -562,21 +562,6 @@ def activity_stream_item(self, activity_type, revision, user_id): } ) - 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 }) - 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 f32486e4ea7..b2e34d928f2 100644 --- a/ckan/model/package_extra.py +++ b/ckan/model/package_extra.py @@ -34,20 +34,6 @@ class PackageExtra(vdm.sqlalchemy.RevisionedObjectMixin, 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, @@ -78,8 +64,7 @@ def _create_extra(key, value): return PackageExtra(key=unicode(key), value=value) _extras_active = vdm.sqlalchemy.stateful.DeferredProperty('_extras', - vdm.sqlalchemy.stateful.StatefulDict, base_modifier=lambda x: x.get_as_of()) + vdm.sqlalchemy.stateful.StatefulDict, base_modifier=lambda x: x.get_as_of()) setattr(_package.Package, 'extras_active', _extras_active) _package.Package.extras = vdm.sqlalchemy.stateful.OurAssociationProxy('extras_active', 'value', creator=_create_extra) - diff --git a/ckan/model/resource.py b/ckan/model/resource.py index 529a7694401..0b0835fa8b0 100644 --- a/ckan/model/resource.py +++ b/ckan/model/resource.py @@ -157,23 +157,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 c1be31eb2c3..22ed06c5ebd 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, From 8c34180098c4a5ecf9904206d825c9b256a96880 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 16 Mar 2018 12:40:22 +0000 Subject: [PATCH 011/103] Restore activity_detail_list to how it was - ActivityDetail is deprecated anyway. --- ckan/logic/action/get.py | 45 +++++----------------------------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 59dd9455b0b..b8cbc0e8c25 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2702,51 +2702,16 @@ def recently_changed_packages_activity_list(context, data_dict): def activity_detail_list(context, data_dict): '''Return an activity's list of activity detail items. - :param id: the id or name of the object (e.g. dataset) - (or activity - DEPRECATED) + :param id: the id of the activity :type id: string - :param object_type: the type of the object being identified. - Accepted values: ``'package'``, ``'activity'`` (deprecated, default) - (``'activity'`` is just there for backward compatibility) - :type object_id: string - :rtype: list of dictionaries + :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') - object_type = data_dict.get('object_type', 'activity') - allowed_object_types = ('package', 'activity') - if object_type not in allowed_object_types: - raise logic.ValidationError('object_type not accepted. Choose from: {}' - .format(allowed_object_types)) - name_or_id = data_dict.get("id") or _get_or_bust(data_dict, 'name_or_id') - if object_type != 'activity': - # i.e. object_type == 'package': - data_dict['include_data'] = True - _check_access('activity_list_show', context, data_dict) - - offset = int(data_dict.get('offset', 0)) - limit = int( - data_dict.get('limit', config.get('ckan.activity_list_limit', 31))) - - _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()) - - return model_dictize.activity_list_dictize(activity_objects, context) - # activity_detail_objects = \ - # model.Session.query(model.ActivityDetail) \ - # .filter_by(object_type=object_type) \ - # .filter_by(object_id=id) \ - # .join(model.Activity) \ - # .order_by(model.Activity.timestamp) - else: - activity_detail_objects = \ - model.ActivityDetail.by_activity_id(activity_id) - if 'package' in activity.activity_type: - pass - _check_access(u'activity_detail_show', context, data_dict) + activity_detail_objects = model.ActivityDetail.by_activity_id(activity_id) return model_dictize.activity_detail_list_dictize( activity_detail_objects, context) From ca9e5975923d219a349ff1c33523218b505e1712 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 16 Mar 2018 14:50:54 +0000 Subject: [PATCH 012/103] include_data not needed on this function after all --- ckan/logic/schema.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ckan/logic/schema.py b/ckan/logic/schema.py index 4189de084c6..88d2cbda2e3 100644 --- a/ckan/logic/schema.py +++ b/ckan/logic/schema.py @@ -607,11 +607,9 @@ def default_dashboard_activity_list_schema(unicode_safe): @validator_args -def default_activity_list_schema( - ignore_missing, not_missing, unicode_safe, boolean_validator): +def default_activity_list_schema(not_missing, unicode_safe): schema = default_pagination_schema() schema['id'] = [not_missing, unicode_safe] - schema['include_data'] = [ignore_missing, boolean_validator] return schema From 133b6c6ed787c29d77798769a952f12f1db340d3 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 23 Mar 2018 13:51:49 +0000 Subject: [PATCH 013/103] Add a diff view --- ckan/config/routing.py | 2 + ckan/controllers/package.py | 42 +++++++++++- ckan/lib/formatters.py | 15 ++++- ckan/lib/helpers.py | 8 ++- ckan/logic/action/get.py | 65 +++++++++++++++++++ .../snippets/activities/changed_package.html | 4 ++ ckan/tests/legacy/lib/test_helpers.py | 30 --------- ckan/tests/lib/test_helpers.py | 38 +++++++++++ 8 files changed, 168 insertions(+), 36 deletions(-) diff --git a/ckan/config/routing.py b/ckan/config/routing.py index 82c7f066f51..d6e4c8972cc 100644 --- a/ckan/config/routing.py +++ b/ckan/config/routing.py @@ -217,6 +217,8 @@ def make_map(): m.connect('dataset_activity', '/dataset/activity/{id}', action='activity', ckan_icon='clock-o') m.connect('/dataset/activity/{id}/{offset}', action='activity') + m.connect('dataset_changes', '/dataset/changes/{activity_id}', + action='changes') m.connect('dataset_groups', '/dataset/groups/{id}', action='groups', ckan_icon='users') m.connect('dataset_resources', '/dataset/resources/{id}', diff --git a/ckan/controllers/package.py b/ckan/controllers/package.py index 7aff4a19ac1..565479da1e3 100644 --- a/ckan/controllers/package.py +++ b/ckan/controllers/package.py @@ -375,11 +375,16 @@ def read(self, id): except NotFound: abort(404, _('Activity not found')) except NotAuthorized: - abort(403, _('Unauthorized to create a package')) + abort(403, _('Unauthorized to view activity data')) try: c.pkg_dict = activity['data']['package'] except KeyError: abort(404, _('Dataset not found')) + if c.pkg_dict['id'] != id: + # the activity is not for the package in the URL - don't allow + # misleading URLs as could be malicious + abort(404, _('Activity not found')) + # Earlier versions of CKAN only stored the package table in the # activity, so add a placeholder for resources, or the template # will crash. @@ -1015,6 +1020,12 @@ def resource_read(self, id, resource_id): ).data['package'] except AttributeError: abort(404, _('Dataset not found')) + + if c.package['id'] != id: + # the activity is not for the package in the URL - don't allow + # misleading URLs as could be malicious + abort(404, _('Activity not found')) + # Don't crash on old activity records, which do not include # resources or extras. c.package.setdefault('resources', []) @@ -1552,3 +1563,32 @@ def resource_datapreview(self, id, resource_id): else: return render(preview_plugin.preview_template(context, data_dict), extra_vars={'dataset_type': dataset_type}) + + def changes(self, activity_id): + ''' + Shows the changes to a dataset in one particular activity stream + item. + ''' + context = { + 'model': model, 'session': model.Session, + 'user': c.user, 'auth_user_obj': c.userobj + } + try: + activity_diff = get_action('activity_diff')( + context, {'id': activity_id, 'object_type': 'package', + 'diff_type': 'html'}) + except NotFound: + abort(404, _('Activity not found')) + except NotAuthorized: + abort(403, _('Unauthorized to view activity data')) + + # 'pkg_dict' needs to go to the templates for page title & breadcrumbs. + # Use the current version of the package, in case the name/title have + # changed, and we need a link to it which works + pkg_id = activity_diff['activities'][1]['data']['package']['id'] + current_pkg_dict = get_action('package_show')(context, {'id': pkg_id}) + + return render('package/changes.html', + extra_vars={'activity_diff': activity_diff, + 'pkg_dict': current_pkg_dict, + }) diff --git a/ckan/lib/formatters.py b/ckan/lib/formatters.py index 6c927a53399..993e924b3ad 100644 --- a/ckan/lib/formatters.py +++ b/ckan/lib/formatters.py @@ -70,15 +70,18 @@ def _month_dec(): _month_sept, _month_oct, _month_nov, _month_dec] -def localised_nice_date(datetime_, show_date=False, with_hours=False): +def localised_nice_date(datetime_, show_date=False, with_hours=False, + with_seconds=False): ''' Returns a friendly localised unicode representation of a datetime. :param datetime_: The date to format :type datetime_: datetime - :param show_date: Show date not 2 days ago etc + :param show_date: Show date not '2 days ago' etc :type show_date: bool :param with_hours: should the `hours:mins` be shown for dates :type with_hours: bool + :param with_seconds: should the `hours:mins:seconds` be shown for dates + :type with_seconds: bool :rtype: sting ''' @@ -132,6 +135,7 @@ def months_between(date1, date2): # actual date details = { + 'sec': int(datetime_.second), 'min': datetime_.minute, 'hour': datetime_.hour, 'day': datetime_.day, @@ -140,7 +144,12 @@ def months_between(date1, date2): 'timezone': datetime_.tzname(), } - if with_hours: + if with_seconds: + return ( + # NOTE: This is for translating dates like `April 24, 2013, 10:45:21 (Europe/Zurich)` + _('{month} {day}, {year}, {hour:02}:{min:02}:{sec:02} ({timezone})') \ + .format(**details)) + elif with_hours: return ( # NOTE: This is for translating dates like `April 24, 2013, 10:45 (Europe/Zurich)` _('{month} {day}, {year}, {hour:02}:{min:02} ({timezone})') \ diff --git a/ckan/lib/helpers.py b/ckan/lib/helpers.py index 6da29d6eec5..03e4b61f7e6 100644 --- a/ckan/lib/helpers.py +++ b/ckan/lib/helpers.py @@ -1358,7 +1358,8 @@ def get_display_timezone(): @core_helper -def render_datetime(datetime_, date_format=None, with_hours=False): +def render_datetime(datetime_, date_format=None, with_hours=False, + with_seconds=False): '''Render a datetime object or timestamp string as a localised date or in the requested format. If timestamp is badly formatted, then a blank string is returned. @@ -1369,6 +1370,8 @@ def render_datetime(datetime_, date_format=None, with_hours=False): :type date_format: string :param with_hours: should the `hours:mins` be shown :type with_hours: bool + :param with_seconds: should the `hours:mins:seconds` be shown + :type with_seconds: bool :rtype: string ''' @@ -1399,7 +1402,8 @@ def render_datetime(datetime_, date_format=None, with_hours=False): return datetime_.strftime(date_format) # the localised date return formatters.localised_nice_date(datetime_, show_date=True, - with_hours=with_hours) + with_hours=with_hours, + with_seconds=with_seconds) @core_helper diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index b8cbc0e8c25..423af7f6279 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -3330,6 +3330,71 @@ def activity_data_show(context, data_dict): 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') + + data_dict['include_data'] = True + _check_access(u'activity_show', 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 = [pformat(obj).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': + 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/templates/snippets/activities/changed_package.html b/ckan/templates/snippets/activities/changed_package.html index ea7fe214344..e216ceb4f2b 100644 --- a/ckan/templates/snippets/activities/changed_package.html +++ b/ckan/templates/snippets/activities/changed_package.html @@ -13,6 +13,10 @@ {{ _('View this version') }} +  |  + + {{ _('View raw changes') }} + {% endif %}

            diff --git a/ckan/tests/legacy/lib/test_helpers.py b/ckan/tests/legacy/lib/test_helpers.py index f60de863e46..5516c2808b6 100644 --- a/ckan/tests/legacy/lib/test_helpers.py +++ b/ckan/tests/legacy/lib/test_helpers.py @@ -23,36 +23,6 @@ def test_extract_markdown(self): assert "Data exposed" in h.markdown_extract(WITH_HTML) assert "collects information" in h.markdown_extract(WITH_UNICODE) - def test_render_datetime(self): - res = h.render_datetime(datetime.datetime(2008, 4, 13, 20, 40, 20, 123456)) - assert_equal(res, 'April 13, 2008') - - def test_render_datetime_with_hours(self): - res = h.render_datetime(datetime.datetime(2008, 4, 13, 20, 40, 20, 123456), with_hours=True) - assert_equal(res, 'April 13, 2008, 20:40 (UTC)') - - def test_render_datetime_but_from_string(self): - res = h.render_datetime('2008-04-13T20:40:20.123456') - assert_equal(res, 'April 13, 2008') - - def test_render_datetime_blank(self): - res = h.render_datetime(None) - assert_equal(res, '') - - def test_render_datetime_year_before_1900(self): - res = h.render_datetime('1875-04-13T20:40:20.123456', date_format='%Y') - assert_equal(res, '1875') - - res = h.render_datetime('1875-04-13T20:40:20.123456', date_format='%y') - assert_equal(res, '75') - - def test_render_datetime_year_before_1900_escape_percent(self): - res = h.render_datetime('1875-04-13', date_format='%%%y') - assert_equal(res, '%75') - - res = h.render_datetime('1875-04-13', date_format='%%%Y') - assert_equal(res, '%1875') - def test_datetime_to_date_str(self): res = datetime.datetime(2008, 4, 13, 20, 40, 20, 123456).isoformat() assert_equal(res, '2008-04-13T20:40:20.123456') diff --git a/ckan/tests/lib/test_helpers.py b/ckan/tests/lib/test_helpers.py index f4881b099ed..9636381e879 100644 --- a/ckan/tests/lib/test_helpers.py +++ b/ckan/tests/lib/test_helpers.py @@ -1,5 +1,6 @@ # encoding: utf-8 +import datetime import nose import pytz import tzlocal @@ -525,6 +526,43 @@ def test_named_timezone(self): eq_(h.get_display_timezone(), pytz.timezone('America/New_York')) +class TestHelpersRenderDatetime(object): + + def test_date(self): + data = datetime.datetime(2008, 4, 13, 20, 40, 59, 123456) + eq_(h.render_datetime(data), 'April 13, 2008') + + def test_with_hours(self): + data = datetime.datetime(2008, 4, 13, 20, 40, 59, 123456) + eq_(h.render_datetime(data, with_hours=True), + 'April 13, 2008, 20:40 (UTC)') + + def test_with_seconds(self): + data = datetime.datetime(2008, 4, 13, 20, 40, 59, 123456) + eq_(h.render_datetime(data, with_seconds=True), + 'April 13, 2008, 20:40:59 (UTC)') + + def test_from_string(self): + data = '2008-04-13T20:40:20.123456' + eq_(h.render_datetime(data), 'April 13, 2008') + + def test_blank(self): + data = None + eq_(h.render_datetime(data), '') + + def test_before_1900(self): + data = '1875-04-13T20:40:20.123456' + eq_(h.render_datetime(data, date_format='%Y'), '1875') + + def test_before_1900_with_2_digit_year(self): + data = '1875-04-13T20:40:20.123456' + eq_(h.render_datetime(data, date_format='%y'), '75') + + def test_escaped_percent(self): + data = '2008-04-13T20:40:20.123456' + eq_(h.render_datetime(data, date_format='%%%Y'), '%2008') + + class TestHelperException(helpers.FunctionalTestBase): @raises(ckan.exceptions.HelperError) From ac5df412067a355abdc1682ef2c696e515ecb12d Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 13 Apr 2018 12:11:50 +0000 Subject: [PATCH 014/103] Improve the look of the diff --- ckan/logic/action/get.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 423af7f6279..85bad20da8e 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -3371,7 +3371,7 @@ def activity_diff(context, data_dict): 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 = [pformat(obj).split('\n') for obj in objs] + obj_lines = [json.dumps(obj, indent=2).split('\n') for obj in objs] # do the diff if diff_type == 'unified': @@ -3381,6 +3381,13 @@ def activity_diff(context, data_dict): 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') From d2fd54766bce8b9818805910708d5de15e8e83f2 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 13 Apr 2018 13:41:44 +0000 Subject: [PATCH 015/103] Improved comments --- ckan/lib/formatters.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ckan/lib/formatters.py b/ckan/lib/formatters.py index 993e924b3ad..c1db2d2e988 100644 --- a/ckan/lib/formatters.py +++ b/ckan/lib/formatters.py @@ -73,6 +73,9 @@ def _month_dec(): def localised_nice_date(datetime_, show_date=False, with_hours=False, with_seconds=False): ''' Returns a friendly localised unicode representation of a datetime. + e.g. '31 minutes ago' + '1 day ago' + 'April 24, 2013' (show_date=True) :param datetime_: The date to format :type datetime_: datetime @@ -146,17 +149,17 @@ def months_between(date1, date2): if with_seconds: return ( - # NOTE: This is for translating dates like `April 24, 2013, 10:45:21 (Europe/Zurich)` + # Example output: `April 24, 2013, 10:45:21 (Europe/Zurich)` _('{month} {day}, {year}, {hour:02}:{min:02}:{sec:02} ({timezone})') \ .format(**details)) elif with_hours: return ( - # NOTE: This is for translating dates like `April 24, 2013, 10:45 (Europe/Zurich)` + # Example output: `April 24, 2013, 10:45 (Europe/Zurich)` _('{month} {day}, {year}, {hour:02}:{min:02} ({timezone})') \ .format(**details)) else: return ( - # NOTE: This is for translating dates like `April 24, 2013` + # Example output: `April 24, 2013` _('{month} {day}, {year}').format(**details)) From a57be3ce0948dd90b8054ef7b79ae859087c7339 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 20 Apr 2018 17:36:38 +0100 Subject: [PATCH 016/103] Fix saving activity for edited datasets --- ckan/lib/activity_streams_session_extension.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ckan/lib/activity_streams_session_extension.py b/ckan/lib/activity_streams_session_extension.py index b0166605e64..4c272226b08 100644 --- a/ckan/lib/activity_streams_session_extension.py +++ b/ckan/lib/activity_streams_session_extension.py @@ -116,6 +116,7 @@ def before_commit(self, session): package, "changed", revision, user_id) if activity is None: continue + activities[package.id] = activity for key, activity in activities.items(): # Emitting activity From 5c1ccb62a96470250a294b5575f68aa61ba92184 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 20 Apr 2018 17:38:01 +0100 Subject: [PATCH 017/103] Add tests for controller --- ckan/logic/action/get.py | 7 +-- ckan/tests/controllers/test_package.py | 74 +++++++++++++++++++++++++- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 85bad20da8e..bf7acc6d8e6 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -3291,10 +3291,11 @@ def activity_show(context, data_dict): def activity_data_show(context, data_dict): - '''Show the data from an item of 'activity' (part of the activity stream). + '''Show the data from an item of 'activity' (part of the activity + stream). - For example you can get just the old version of a dataset, without the - activity stream info of who and when the version was created. + 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 diff --git a/ckan/tests/controllers/test_package.py b/ckan/tests/controllers/test_package.py index 162f9d41693..7cda3508b86 100644 --- a/ckan/tests/controllers/test_package.py +++ b/ckan/tests/controllers/test_package.py @@ -6,7 +6,8 @@ assert_not_equal, assert_raises, assert_true, - assert_in + assert_not_in, + assert_in, ) from mock import patch, MagicMock @@ -1747,3 +1748,74 @@ def test_dataset_read(self): id=dataset['id']) response = app.get(url) assert_in(dataset['title'], response) + + +class TestActivity(helpers.FunctionalTestBase): + ''' + We don't test the detail of the different kinds of activities - that is + tested at the logic layer level. + ''' + + def test_simple(self): + '''Checking the template shows the activity stream.''' + app = self._get_test_app() + + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for(controller='package', + action='activity', + id=dataset['id']) + response = app.get(url) + assert_in(dataset['title'], response) + assert_in('created the dataset', response) + + def test_admin_can_see_old_versions(self): + app = self._get_test_app() + user = factories.User() + env = {'REMOTE_USER': user['name'].encode('ascii')} + dataset = factories.Dataset(user=user) + + url = url_for(controller='package', + action='activity', + id=dataset['id']) + response = app.get(url, extra_environ=env) + assert_in('View this version', response) + + def test_public_cant_see_old_versions(self): + app = self._get_test_app() + user = factories.User() + dataset = factories.Dataset(user=user) + + url = url_for(controller='package', + action='activity', + id=dataset['id']) + response = app.get(url) + assert_not_in('View this version', response) + + def test_admin_can_see_changes(self): + app = self._get_test_app() + user = factories.User() + env = {'REMOTE_USER': user['name'].encode('ascii')} + dataset = factories.Dataset() # activities by system user aren't shown + dataset['title'] = 'Changed' + result = helpers.call_action('package_update', **dataset) + + url = url_for(controller='package', + action='activity', + id=dataset['id']) + response = app.get(url, extra_environ=env) + assert_in('View raw changes', response) + + def test_public_cant_see_changes(self): + app = self._get_test_app() + user = factories.User() + dataset = factories.Dataset() # activities by system user aren't shown + dataset['title'] = 'Changed' + result = helpers.call_action('package_update', **dataset) + + url = url_for(controller='package', + action='activity', + id=dataset['id']) + response = app.get(url) + assert_not_in('View raw changes', response) From ee6ce10e4dd95d794f4f7194ddf4e9c1be767ec4 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Apr 2018 13:56:57 +0100 Subject: [PATCH 018/103] Add template for changes page. --- ckan/templates/package/changes.html | 36 +++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 ckan/templates/package/changes.html diff --git a/ckan/templates/package/changes.html b/ckan/templates/package/changes.html new file mode 100644 index 00000000000..af5e894ff19 --- /dev/null +++ b/ckan/templates/package/changes.html @@ -0,0 +1,36 @@ +{% extends "package/base.html" %} + +{% block subtitle %}{{ pkg_dict.name }} - Changes - {{ 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 %} From a8d7351bff771b45136f1357d02f55ffffbce76c Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Apr 2018 20:23:57 +0000 Subject: [PATCH 019/103] Add migration script for older activities --- ckan/logic/action/get.py | 8 +- ckan/migration/migrate_revisions.py | 121 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 ckan/migration/migrate_revisions.py diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index bf7acc6d8e6..1969b054b5b 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -2574,6 +2574,7 @@ def package_activity_list(context, data_dict): # authorized to read. data_dict['object_type'] = 'package' data_dict['include_data'] = False + include_hidden_activity = asbool(context.get('include_hidden_activity')) _check_access('activity_list_show', context, data_dict) model = context['model'] @@ -2589,8 +2590,11 @@ 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, include_data=data_dict['include_data']) diff --git a/ckan/migration/migrate_revisions.py b/ckan/migration/migrate_revisions.py new file mode 100644 index 00000000000..80cd446fca0 --- /dev/null +++ b/ckan/migration/migrate_revisions.py @@ -0,0 +1,121 @@ +''' +Migrates revisions into the activity stream, to allow you to view old versions +of datasets and changes (diffs) between them. +''' + +# This cost 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. + +# 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. + +import argparse + +# 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('get_site_user')( + {'model': model, 'ignore_auth': True}, {}) + _context = {'model': model, 'session': model.Session, + 'user': user['name']} + return _context + + +def migrate_all_datasets(): + import ckan.logic as logic + dataset_names = logic.get_action('package_list')(get_context(), {}) + num_datasets = len(dataset_names) + for i, dataset_name in enumerate(dataset_names): + print '{}/{} {}'.format(i + 1, num_datasets, dataset_name) + migrate_dataset(dataset_name) + + +def migrate_dataset(dataset_name): + import ckan.logic as logic + from ckan import model + + context = get_context() + # '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 thes 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. + context['include_hidden_activity'] = True + package_activity_stream = logic.get_action('package_activity_list')( + context, {'id': dataset_name}) + num_activities = len(package_activity_stream) + if not num_activities: + print(' No activities') + + context['for_view'] = True + for i, activity in enumerate(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 ' activity {}/{} {}'.format( + i + 1, num_activities, activity['timestamp']) + + # get the dataset as it was at this revision + context['revision_id'] = activity['revision_id'] + # call package_show just as we do in activity_stream_item(), only + # with a revision_id + dataset = logic.get_action('package_show')( + context, {'id': activity['object_id'], 'include_tracking': True}) + # 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 'revision_timestamp' in (dataset.get('organization') or {}): + del dataset['organization']['revision_timestamp'] + for res in dataset['resources']: + if 'revision_timestamp' in res: + del res['revision_timestamp'] + + actor = model.Session.query(model.User).get(activity['user_id']) + actor_name = actor.name if actor else activity['user_id'] + + # add the data to the Activity, just as we do in activity_stream_item() + data = { + 'package': dataset, + 'actor': actor_name, + } + # there are no action functions for Activity, and anyway the ORM would + # be faster + activity_obj = model.Session.query(model.Activity).get(activity['id']) + if 'resources' in activity_obj.data.get('package', {}): + print ' Full dataset already recorded - no action' + else: + activity_obj.data = data + #print ' {} dataset {}'.format(actor_name, repr(dataset)) + if model.Session.dirty: + model.Session.commit() + print ' saved' + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(usage=__doc__) + parser.add_argument('-c', '--config', help='CKAN config file (.ini)') + parser.add_argument('--dataset', help='just migrate this particular ' + 'dataset - specify its name') + args = parser.parse_args() + assert args.config, 'You must supply a --config' + from ckan.lib.cli import load_config + print 'Loading config' + load_config(args.config) + if not args.dataset: + migrate_all_datasets() + else: + migrate_dataset(args.dataset) From b27b822cd169d8e11205563974dd456648248807 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Apr 2018 20:59:39 +0000 Subject: [PATCH 020/103] Add standard migration, to remind the site admin to run the migrate_revisions script. --- ckan/migration/migrate_revisions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ckan/migration/migrate_revisions.py b/ckan/migration/migrate_revisions.py index 80cd446fca0..9ff091b7226 100644 --- a/ckan/migration/migrate_revisions.py +++ b/ckan/migration/migrate_revisions.py @@ -99,7 +99,7 @@ def migrate_dataset(dataset_name): print ' Full dataset already recorded - no action' else: activity_obj.data = data - #print ' {} dataset {}'.format(actor_name, repr(dataset)) + # print ' {} dataset {}'.format(actor_name, repr(dataset)) if model.Session.dirty: model.Session.commit() print ' saved' From bb2e0aaa72a10d6ffbb0022bc5870b8fad7d42d2 Mon Sep 17 00:00:00 2001 From: David Read Date: Fri, 27 Apr 2018 21:04:48 +0000 Subject: [PATCH 021/103] Diff - sort keys otherwise reordering looks like a change in the diff --- ckan/logic/action/get.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ckan/logic/action/get.py b/ckan/logic/action/get.py index 1969b054b5b..b150c5d713d 100644 --- a/ckan/logic/action/get.py +++ b/ckan/logic/action/get.py @@ -3376,7 +3376,8 @@ def activity_diff(context, data_dict): 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).split('\n') for obj in objs] + obj_lines = [json.dumps(obj, indent=2, sort_keys=True).split('\n') + for obj in objs] # do the diff if diff_type == 'unified': From 827c329032ad32b63b4c69768687558945fe24b6 Mon Sep 17 00:00:00 2001 From: David Read Date: Sat, 28 Apr 2018 06:24:53 +0100 Subject: [PATCH 022/103] resources page is for editing - wouldnt be viewing an old version --- ckan/templates-bs2/package/resources.html | 2 +- ckan/templates/package/resources.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ckan/templates-bs2/package/resources.html b/ckan/templates-bs2/package/resources.html index 19af7721382..dcb708e6d40 100644 --- a/ckan/templates-bs2/package/resources.html +++ b/ckan/templates-bs2/package/resources.html @@ -11,7 +11,7 @@ {% block primary_content_inner %} {% if pkg.resources %} - {%endif%} {% endblock %} + {% endif %} + {% endblock %} {% endif %} {% endblock %} @@ -101,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 %} @@ -166,6 +169,7 @@

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

    {% endblock %} {% endblock %} + {% endif %} {% endblock %} {% endblock %} @@ -221,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/snippets/resource_item.html b/ckan/templates-bs2/package/snippets/resource_item.html index 6b80043e05e..888f267e4ec 100644 --- a/ckan/templates-bs2/package/snippets/resource_item.html +++ b/ckan/templates-bs2/package/snippets/resource_item.html @@ -1,5 +1,5 @@ {% 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, **({'activity_id': request.args['activity_id']} if 'activity_id' in request.args else {})) %} +{% 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 %} diff --git a/ckan/templates-bs2/package/snippets/resources.html b/ckan/templates-bs2/package/snippets/resources.html index 59937200b34..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 1c8092c8649..c54d5f17334 100644 --- a/ckan/templates-bs2/package/snippets/resources_list.html +++ b/ckan/templates-bs2/package/snippets/resources_list.html @@ -18,15 +18,13 @@

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

    {% block resource_list_inner %} {% 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']}) and not is_activity_archive %} {% trans url=h.url_for('resource.new', id=pkg.name) %} - {% if h.check_access('resource_create', {'package_id': pkg['id']}) %} - {% trans url=h.url_for('resource.new', id=pkg.name) %}

    This dataset has no data, why not add some?

    {% endtrans %} {% else %} 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/read_base.html b/ckan/templates/package/read_base.html index 4f6ac4beca5..0f0a458e791 100644 --- a/ckan/templates/package/read_base.html +++ b/ckan/templates/package/read_base.html @@ -18,24 +18,9 @@ {% 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 %} @@ -48,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_read.html b/ckan/templates/package/resource_read.html index f7d23c5a5b0..4d89564836d 100644 --- a/ckan/templates/package/resource_read.html +++ b/ckan/templates/package/resource_read.html @@ -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 %} @@ -95,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 %} @@ -158,6 +160,7 @@

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

    {% endblock %} {% endblock %} + {% endif %} {% endblock %} {% endblock %} @@ -213,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/resource_item.html b/ckan/templates/package/snippets/resource_item.html index c9f6b54d746..ac9a6ed75a5 100644 --- a/ckan/templates/package/snippets/resource_item.html +++ b/ckan/templates/package/snippets/resource_item.html @@ -1,5 +1,5 @@ {% 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, **({'activity_id': request.args['activity_id']} if 'activity_id' in request.args else {})) %} +{% 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 %} diff --git a/ckan/templates/package/snippets/resources.html b/ckan/templates/package/snippets/resources.html index 805e1f13757..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 f3a0aa30f74..8041100f42a 100644 --- a/ckan/templates/package/snippets/resources_list.html +++ b/ckan/templates/package/snippets/resources_list.html @@ -18,7 +18,7 @@

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

    {% block resource_list_inner %} {% 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 %} From d7fda7acb1c6e17540b29dd4eaf79ba65fe96f96 Mon Sep 17 00:00:00 2001 From: David Read Date: Sat, 2 Feb 2019 23:09:52 +0000 Subject: [PATCH 067/103] Fix comma missing in resource_read.html and template docs * Don't display resource preview when viewing an old dataset version (i.e. when is_activity_archive=True) because the datastore will be the latest version of the data, not the old version that matches the metadata. * Fix comma missing in ckan/templates/package/resource_read.html * Improve docs of the package html --- ckan/templates-bs2/package/snippets/info.html | 2 +- .../package/snippets/resource_item.html | 14 ++++++++++++++ .../package/snippets/resources_list.html | 4 ++-- ckan/templates-bs2/package/snippets/tags.html | 10 ++++++++++ ckan/templates-bs2/snippets/tag_list.html | 12 +++++++++--- ckan/templates/package/resource_read.html | 2 +- ckan/templates/package/snippets/info.html | 2 +- .../package/snippets/resource_item.html | 16 +++++++++++++++- .../package/snippets/resources_list.html | 4 ++-- ckan/templates/package/snippets/tags.html | 10 ++++++++++ ckan/templates/snippets/tag_list.html | 10 ++++++++-- ckan/views/dataset.py | 5 +++-- ckan/views/resource.py | 4 +++- 13 files changed, 79 insertions(+), 16 deletions(-) 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 888f267e4ec..b702b753717 100644 --- a/ckan/templates-bs2/package/snippets/resource_item.html +++ b/ckan/templates-bs2/package/snippets/resource_item.html @@ -1,3 +1,17 @@ +{# + 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.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 {})) %} diff --git a/ckan/templates-bs2/package/snippets/resources_list.html b/ckan/templates-bs2/package/snippets/resources_list.html index c54d5f17334..8c33546d6ca 100644 --- a/ckan/templates-bs2/package/snippets/resources_list.html +++ b/ckan/templates-bs2/package/snippets/resources_list.html @@ -1,8 +1,8 @@ {# 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: 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/snippets/tag_list.html b/ckan/templates-bs2/snippets/tag_list.html index 253220b1e21..1450f4f2236 100644 --- a/ckan/templates-bs2/snippets/tag_list.html +++ b/ckan/templates-bs2/snippets/tag_list.html @@ -1,7 +1,13 @@ {# -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 %}