Skip to content

Commit

Permalink
Custom activities can now be rendered, using a fallback template.
Browse files Browse the repository at this point in the history
  • Loading branch information
David Read committed Feb 15, 2019
1 parent 36a387c commit 73df4ba
Show file tree
Hide file tree
Showing 5 changed files with 112 additions and 12 deletions.
30 changes: 22 additions & 8 deletions ckan/lib/base.py
Expand Up @@ -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,
Expand Down Expand Up @@ -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<!-- Snippet %s start -->\n%s\n<!-- Snippet %s end -->\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<!-- Snippet %s start -->\n%s\n<!-- Snippet %s end -->\n'
% (template_name, output, template_name))
return literal(output)
except TemplateNotFound:
continue
else:
raise TemplateNotFound


def render_jinja2(template_name, extra_vars):
Expand Down
6 changes: 3 additions & 3 deletions ckan/lib/jinja_extensions.py
Expand Up @@ -255,7 +255,8 @@ def make_call_node(*kw):
class SnippetExtension(BaseExtension):
''' Custom snippet tag
{% snippet <template_name> [, <keyword>=<value>].. %}
{% snippet <template_name> [, <fallback_template_name>]...
[, <keyword>=<value>]... %}
see lib.helpers.snippet() for more details.
'''
Expand All @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions 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
#}
<li class="item {{ activity.activity_type|replace(' ', '-')|lower }}">
<i class="fa icon fa-users"></i>
<p>
{{ _('{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 %}
<br />
<span class="date">
{{ h.time_ago_from_timestamp(activity.timestamp) }}
</span>
</p>
</li>
3 changes: 2 additions & 1 deletion ckan/templates/snippets/activity_stream.html
Expand Up @@ -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,
Expand Down
48 changes: 48 additions & 0 deletions ckan/tests/controllers/test_package.py
Expand Up @@ -9,13 +9,15 @@
assert_not_in,
assert_in,
)
import mock

from ckan.lib.helpers import url_for

import ckan.model as model
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
Expand Down Expand Up @@ -1979,3 +1981,49 @@ def test_legacy_changed_package_activity(self):
assert_in('updated the dataset', response)
assert_in('<a href="/dataset/{}">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('<a href="/user/{}">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)

0 comments on commit 73df4ba

Please sign in to comment.