diff --git a/ckan/lib/base.py b/ckan/lib/base.py index 15f1fd62225..4b1553cdd4b 100644 --- a/ckan/lib/base.py +++ b/ckan/lib/base.py @@ -9,13 +9,13 @@ import inspect import sys - from pylons import cache from pylons.controllers import WSGIController from pylons.controllers.util import abort as _abort from pylons.decorators import jsonify from pylons.templating import cached_template, pylons_globals from webhelpers.html import literal +from jinja2.exceptions import TemplateNotFound from flask import ( render_template as flask_render_template, @@ -76,17 +76,31 @@ def abort(status_code=None, detail='', headers=None, comment=None): comment=comment) -def render_snippet(template_name, **kw): +def render_snippet(*template_names, **kw): ''' Helper function for rendering snippets. Rendered html has comment tags added to show the template used. NOTE: unlike other render functions this takes a list of keywords instead of a dict for - the extra template variables. ''' + the extra template variables. + + :param template_names: the template to render, optionally with fallback + values, for when the template can't be found. For each, specify the + relative path to the template inside the registered tpl_dir. + :type template_names: str + :param kw: extra template variables to supply to the template + :type kw: named arguments of any type that are supported by the template + ''' - output = render(template_name, extra_vars=kw) - if config.get('debug'): - output = ('\n\n%s\n\n' - % (template_name, output, template_name)) - return literal(output) + for template_name in template_names: + try: + output = render(template_name, extra_vars=kw) + if config.get('debug'): + output = ('\n\n%s\n\n' + % (template_name, output, template_name)) + return literal(output) + except TemplateNotFound: + continue + else: + raise TemplateNotFound def render_jinja2(template_name, extra_vars): diff --git a/ckan/lib/jinja_extensions.py b/ckan/lib/jinja_extensions.py index 82851fd7919..8520e7bd862 100644 --- a/ckan/lib/jinja_extensions.py +++ b/ckan/lib/jinja_extensions.py @@ -255,7 +255,8 @@ def make_call_node(*kw): class SnippetExtension(BaseExtension): ''' Custom snippet tag - {% snippet [, =].. %} + {% snippet [, ]... + [, =]... %} see lib.helpers.snippet() for more details. ''' @@ -264,8 +265,7 @@ class SnippetExtension(BaseExtension): @classmethod def _call(cls, args, kwargs): - assert len(args) == 1 - return base.render_snippet(args[0], **kwargs) + return base.render_snippet(*args, **kwargs) class UrlForStaticExtension(BaseExtension): ''' Custom url_for_static tag for getting a path for static assets. diff --git a/ckan/templates/snippets/activities/fallback.html b/ckan/templates/snippets/activities/fallback.html new file mode 100644 index 00000000000..b119c295b36 --- /dev/null +++ b/ckan/templates/snippets/activities/fallback.html @@ -0,0 +1,37 @@ +{# + Fallback template for displaying an activity. + It's not pretty, but it is better than TemplateNotFound. + + Params: + activity - the Activity dict + can_show_activity_detail - whether we should render detail about the activity (i.e. "as it was" and diff, alternatively will just display the metadata about the activity) + id - the id or current name of the object (e.g. package name, user id) + ah - dict of template macros to render linked: actor, dataset, organization, user, group +#} +
  • + +

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

    +
  • diff --git a/ckan/templates/snippets/activity_stream.html b/ckan/templates/snippets/activity_stream.html index 9c182c8c1f8..640d7eebc44 100644 --- a/ckan/templates/snippets/activity_stream.html +++ b/ckan/templates/snippets/activity_stream.html @@ -45,7 +45,8 @@ {% for activity in activity_stream %} {%- snippet "snippets/activities/{}.html".format( activity.activity_type.replace(' ', '_') - ), activity=activity, can_show_activity_detail=can_show_activity_detail, ah={ + ), "snippets/activities/fallback.html", + activity=activity, can_show_activity_detail=can_show_activity_detail, ah={ 'actor': actor, 'dataset': dataset, 'organization': organization, diff --git a/ckan/tests/controllers/test_package.py b/ckan/tests/controllers/test_package.py index 386e93e0818..30e9703657e 100644 --- a/ckan/tests/controllers/test_package.py +++ b/ckan/tests/controllers/test_package.py @@ -9,6 +9,7 @@ assert_not_in, assert_in, ) +import mock from ckan.lib.helpers import url_for @@ -16,6 +17,7 @@ import ckan.model.activity as activity_model import ckan.plugins as p import ckan.lib.dictization as dictization +from ckan.logic.validators import object_id_validators, package_id_exists import ckan.tests.helpers as helpers import ckan.tests.factories as factories @@ -1979,3 +1981,49 @@ def test_legacy_changed_package_activity(self): assert_in('updated the dataset', response) assert_in('Test Dataset'.format(dataset['id']), response) + + # ckanext-canada uses their IActivity to add their custom activity to the + # list of validators: https://github.com/open-data/ckanext-canada/blob/6870e5bc38a04aa8cef191b5e9eb361f9560872b/ckanext/canada/plugins.py#L596 + # but it's easier here to just hack patch it in + @mock.patch( + 'ckan.logic.validators.object_id_validators', dict( + object_id_validators.items() + + [('changed datastore', package_id_exists)]) + ) + def test_custom_activity(self): + '''Render a custom activity + ''' + app = self._get_test_app() + + user = factories.User() + organization = factories.Organization( + users=[{'name': user['id'], 'capacity': 'admin'}] + ) + dataset = factories.Dataset(owner_org=organization['id'], user=user) + resource = factories.Resource(package_id=dataset['id']) + self._clear_activities() + + # Create a custom Activity object. This one is inspired by: + # https://github.com/open-data/ckanext-canada/blob/master/ckanext/canada/activity.py + activity_dict = { + 'user_id': user['id'], + 'object_id': dataset['id'], + 'activity_type': 'changed datastore', + 'data': { + 'resource_id': resource['id'], + 'pkg_type': dataset['type'], + 'resource_name': 'june-2018', + 'owner_org': organization['name'], + 'count': 5, + } + } + helpers.call_action('activity_create', **activity_dict) + + url = url_for('dataset.activity', + id=dataset['id']) + response = app.get(url) + assert_in('Mr. Test User'.format(user['name']), + response) + # it renders the activity with fallback.html, since we've not defined + # changed_datastore.html in this case + assert_in('changed datastore', response)