Compare commits

...

No commits in common. "master" and "debian" have entirely different histories.

25 changed files with 2 additions and 1418 deletions

View File

@ -1,2 +0,0 @@
cmsplugin-blurp is entirely under the copyright of Entr'ouvert and distributed
under the license AGPLv3 or later.

View File

@ -1,2 +0,0 @@
include COPYING
recursive-include src *.mo *.po

313
README
View File

@ -1,313 +0,0 @@
Settings
========
You must declare blocks in a dictionnary setting named
CMS_PLUGIN_BLURP_RENDERERS, each block define a name, a renderer
class and its configuration. The key of the dictionnary define the
slug of each renderer instance, and the value associated to this
slug must be a dictionnary containing at least a key called 'name'
containing the human name of this instance.
Renderer
========
A renderer is a class with the following interface::
class Renderer(object):
def __init__(self, slug, config):
pass
def render(self, context):
'''Return the context to render the template'''
pass
def render_template(self):
'''Return a template path or a Template object'''
pass
The render method must return a context which will be passed to its
template, the render_template method must return template path or a
Django Template object.
You can also define the following class method::
@classmethod
def check(cls, config)
'''Validate the config dictionnary and yield an ASCII string for each error'''
pass
You can raise ImproperlyConfigured when the configuration does not validate.
There is two abstract helper classes:
- `cmsplugin_blurp.renderers.base.BaseRenderer`
which provide abstract method for checking that `render()` and
`render_template()` are properly overriden and a generic
`check()` method which call the `check_config()` config method
which must return an iterator yielding strings if errors are
found
- `cmsplugin_blurp.renderers.template.TemplateRenderer`
an abstract subclass of the `BaseRenderer` which provide a
generic implementation of `render_template()` which extract the
template path from the configuration dictionnary using the key
`template_name` and if it is not found return a template parsed from the
value of the key `template`.
Common configuration keys
=========================
- ``ajax`` if True this key indicate to the base plugin to render the plugin using an AJAX request.
Requires jQuery to be loaded previously by the page using the plugin.
You must add the ``cmsplugin_blurp.urls`` to your urls::
...
url(r'^cmsplugin_blurp/', include('cmsplugin_blurp.urls')),
...
- ``ajax_refresh`` if more than zero it indicates the time between refresh of
the plugin content using the AJAX request otherwise the content is never
refreshed after the first loading.
Static renderer
===============
The simplest renderer is the static renderer you can configure one like this::
CMS_PLUGIN_PLUGIN_BLURP_RENDERERS = {
'homepage_text': {
'name': u'Homepage text',
'class': 'cmsplugin_blurp.renderers.static.Renderer',
'content': u'This is the text for the homepage',
'template': 'homepage_text.html',
}
}
The template `homepage_text.html` could look like this::
{{ config.content }}
Data source renderer
====================
It load one or more local (using a `file://...` URL) or remote file (using
an `http://...` or `https://...` URL) and parses them using the
following parsers:
- a json parser using the `json` package,
- an XML parser using the `etree.ElementTree` package,
- a RSS parser using the `feedparser` package feedparser,
- a CSV parser using the `csv` package.
The resulting data structure can be cached, in this case loading is
asynchronous using a thread.
The config dictonnary can contain the following keys:
- `name`, the human name of this renderer instance,
- `source`, a list of dictionnary defining the remote files, the
content of the dictionnary is described later,
- `template`, the template in which to render the data sources, it
will receive a variable named `data_sources` in its context
containing property named after the `slug` field of each source.
A source definition is a dictionnary containing the following keys:
- `slug`, the field name to hold this source parsed value in the
template, for example with this configuration:
...
'slug': 'source1',
...
you can access it with this template fragment:
{{ data_sources.source1 }}
- `url`, the URL of the file for this source, the scheme file://,
http://, and https:// are supported,
- `auth_mech`, whether an authentication mechanism is required by
the http[s]:// URL, it can be `hmac-sha1`, `hmac-sha256` or
`oauth2`. The HMAC mechanism is specified later; the OAuth2
mechanisme is the classical OAuth2 HTTP bearer authentication
mechanism but it prequires that you are using django-allauth and
that an access token for the provider `authentic2` can be
retrieved for the current user,
- `signature_key`, when using the HMAC authentication mechanism it
holds the secret key used to sign the exchanges,
- `async`, if True make refreshing the cache asynchronous (using a thread),
beware that if the cache is currently empty a synchronous update will be
done, lock are used to limit update thread to one by URL, but it you use
a worker engine their could be multiple thread trying to update the same
cache in different workers, value is optional and its default is False,
- `timeout`, a timeout for making the HTTP request, it is optional
and it default to 10 seconds,
- `refresh`, how long to cache the parsed value of the source, it
is optional and it defaults to 3600 seconds,
- `verify_certificate`, when the scheme of URL is https, it
indicates whether to check the SSL certificate against configured
certifate auhtorities, it is optional and defaults to True,
- `allow_redirects`, whether to follow HTTP redirects when getting
the data source file, it is optional and defaults to False,
- `parser_type`, how to parse the loaded file, it can be `json`,
`xml`, `rss`, 'csv' or 'raw' if you do not want any parsing to be
done, it is optional and defaults to 'raw',
- `content_type`, when doing an HTTP request it configures the
content of the `Accept` header, it is optional and automatically
set using the `parser_type` value.
- `limit`, when parsing an RSS file it limits the returned to first
`limit` entries sorted by date, it is optional and defaults to 0
meaning no limit,
- `csv_params`, when parsing a csv file this dictionnary is passed
as keyword arguments to the `reader()` or `DictReader()`
constructors, depending on whether the `fieldnames` arguments is
present,
- `user_context`, whether the user must be part of the cache key. For retro
compatibility If authentication mechanism is OAuth2, it defaults to True
otherwise to False.
Exemple with the JSON parser
----------------------------
The configuration::
CMS_PLUGIN_BLURP_RENDERERS = {
'json': {
'name': u'My JSON content',
'class': 'cmsplugin_blurp.renderer.data_source.Renderer',
'sources': [
{
'slug': 'json_source',
'url': 'http://example.net/file.json',
'parser_type': 'json',
'auth_mech': 'hmac-sha1',
'signature_key': 'abcdefgh0123',
'refresh': 600,
}
]
'template': 'my-json-block.html',
}
}
The `my-json-block.html` template::
<dl>
{% for key, value in data_sources.json_source.iteritems %}
<dt>{{ key }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
Exemple with the CSV parser
---------------------------
We suppose that the file `/var/spool/data/timesheet.csv` contains
the following datas::
Monday,"10-12,14-17"
Tuesday,"10-12,14-18"
....
You can present this file using this configuration::
CMS_PLUGIN_BLURP_RENDERERS = {
'timesheet': {
'name': u'Timesheet of our organization',
'class': 'cmsplugin_blurp.renderer.data_source.Renderer',
'sources': [
{
'slug': 'timesheet',
'url': 'file:///var/spool/data/timesheet.csv',
'parser_type': 'csv',
'refresh': 86400,
'csv_params': {
'fieldnames': [
'day',
'opening_hours',
]
}
}
],
'template': 'timesheet.html',
}
}
and the following template::
<table>
<thead>
<tr><td>Day</td><td>Opening hours</td></tr>
</thead>
<tbody>
{% for row in data_sources.timesheet %}
<tr><td>{{ row.day }}</td><td>{{ row.opening_hours }}</td></tr>
{% endfor %}
</tbody>
</table>
SQL Renderer
============
Configuration::
CMS_PLUGIN_BLURP_RENDERERS = {
'student_table': {
'name': u'Table of students',
'class': 'cmsplugin_blurp.renderer.sql.Renderer',
'url': 'postgresql://scott:tiger@localhost:5432/mydatabase',
'views': {
'students': {
'query': 'SELECT name, age, birthdate FROM student WHERE class_id = :class_id',
'bindparams': {
'class_id': 12
}
}
}
'template': 'student-table.html',
}
}
Template::
<!-- student-table.html -->
<table>
{% for row in students %}
<tr>
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
<td>{{ row.birthdate }}</td>
</tr>
{% endfor %}
</table>
Template tag
============
render_blurp
------------
You can render a block in any template using the template tag ``render_blurp``:
{% load blurp_tags %}
{% render_blurp "student_table" %}
blurp block tag
---------------
You can insert the context generated by a blurp in your current template to do
the templating yourself, beware that you will lose ajaxification and dynamic
reloading if you use this tag as we cannot send your inline template to the
ajax endpoint::
{% load blurp_tags %}
{% blurp "student_table %}
{% for row in students %}
<tr>
<td>{{ row.name }}</td>
<td>{{ row.age }}</td>
<td>{{ row.birthdate }}</td>
</tr>
{% endfor %}
{% endblurp %}

2
debian/control vendored
View File

@ -11,7 +11,9 @@ Package: python-django-cmsplugin-blurp
Architecture: all
Depends: ${misc:Depends},
python (>= 2.6),
python-django-cms (>= 3),
python-django (>= 1.5),
python-django-classy-tags,
python-feedparser
Description: Django CMS plugin framework

113
setup.py
View File

@ -1,113 +0,0 @@
#! /usr/bin/env python
''' Setup script for cmsplugin-blurp
'''
import os
import subprocess
from setuptools import setup, find_packages
from setuptools.command.install_lib import install_lib as _install_lib
from setuptools.command.sdist import sdist
from distutils.command.build import build as _build
from distutils.cmd import Command
class compile_translations(Command):
description = 'compile message catalogs to MO files via django compilemessages'
user_options = []
def initialize_options(self):
pass
def finalize_options(self):
pass
def run(self):
import os
import sys
try:
from django.core.management import call_command
for path in ['src/cmsplugin_blurp/']:
curdir = os.getcwd()
os.chdir(os.path.realpath(path))
call_command('compilemessages')
os.chdir(curdir)
except ImportError:
print
sys.stderr.write('!!! Please install Django >= 1.7 to build translations')
print
class build(_build):
sub_commands = [('compile_translations', None)] + _build.sub_commands
class eo_sdist(sdist):
sub_commands = [('compile_translations', None)] + sdist.sub_commands
def run(self):
if os.path.exists('VERSION'):
os.remove('VERSION')
version = get_version()
version_file = open('VERSION', 'w')
version_file.write(version)
version_file.close()
sdist.run(self)
if os.path.exists('VERSION'):
os.remove('VERSION')
class install_lib(_install_lib):
def run(self):
self.run_command('compile_translations')
_install_lib.run(self)
def get_version():
if os.path.exists('VERSION'):
version_file = open('VERSION', 'r')
version = version_file.read()
version_file.close()
return version
if os.path.exists('.git'):
p = subprocess.Popen(['git', 'describe', '--dirty', '--match=v*'], stdout=subprocess.PIPE)
result = p.communicate()[0]
if p.returncode == 0:
version = result.split()[0][1:]
version = version.replace('-', '.')
return version
return '0'
setup(name="django-cmsplugin-blurp",
version=get_version(),
license="AGPLv3 or later",
description="",
long_description=file('README').read(),
url="http://dev.entrouvert.org/projects/django-cmsplugin-blurp/",
author="Entr'ouvert",
author_email="info@entrouvert.org",
maintainer="Benjamin Dauvergne",
maintainer_email="bdauvergne@entrouvert.com",
packages=find_packages('src'),
install_requires=[
'django>=1.8',
'django-classy-tags',
],
package_dir={
'': 'src',
},
package_data={
'cmsplugin_blurp': [
'locale/fr/LC_MESSAGES/*.po',
'templates/cmsplugin_blurp/*',
'tests_data/*'
],
},
tests_require=[
'nose>=0.11.4',
],
dependency_links=[],
cmdclass={
'build': build,
'install_lib': install_lib,
'compile_translations': compile_translations,
'sdist': eo_sdist,
},
)

View File

@ -1,17 +0,0 @@
import sys
class AppSettings(object):
__PREFIX = 'CMS_PLUGIN_BLURP_'
__DEFAULTS = {
'RENDERERS': (),
}
def __getattr__(self, name):
from django.conf import settings
if name not in self.__DEFAULTS:
raise AttributeError
return getattr(settings, self.__PREFIX + name, self.__DEFAULTS[name])
app_settings = AppSettings()
app_settings.__name__ = __name__
sys.modules[__name__] = app_settings

View File

@ -1,21 +0,0 @@
# cmsplugin_blurp
# Copyright (C) 2014 Entr'ouvert
# This file is distributed under the same license as the cmsplugin-blurp package.
# Benjamin Dauvergne <bdauvergne@entrouvert.com>, 2014.
#
msgid ""
msgstr ""
"Project-Id-Version: cmsplugin-blurp 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2014-06-27 19:43+0200\n"
"PO-Revision-Date: 2014-06-30 16:06+0200\n"
"Last-Translator: Benjamin Dauvergne <bdauvergne@entrouvert.com>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: utils.py:9
msgid "{name} using template {template}"
msgstr "{name} via le modèle {template}"

View File

@ -1,42 +0,0 @@
import abc
from django.core.exceptions import ImproperlyConfigured
class BaseRenderer(object):
'''A renderer receive a configuration and provider a render
method which produce the content to render
'''
__metaclass__ = abc.ABCMeta
def __init__(self, slug, config):
self.slug = slug
self.config = config
self.check(config)
@classmethod
def check(cls, config):
errors = cls.check_config(config)
if errors is None:
return
errors = list(errors)
if errors:
raise ImproperlyConfigured('{0} configuration errors: {1} {2!r}'.format(
cls.__name__,
', '.join(list(errors)),
config))
@classmethod
def check_config(cls, config):
if not 'name' in config:
yield 'name key is missing'
@abc.abstractmethod
def render(self, context):
'''Return the context to render the template'''
pass
@abc.abstractmethod
def render_template(self):
'''Return a template path or a Template object'''
pass

View File

@ -1,420 +0,0 @@
import logging
import hashlib
from xml.etree import ElementTree as ET
import time
import threading
import pprint
import feedparser
import requests
from requests.exceptions import RequestException, HTTPError, Timeout
from django.core.cache import cache
from django.conf import settings
from . import signature, template
log = logging.getLogger(__name__)
try:
from collections import OrderedDict
except ImportError:
OrderedDict = dict
class Renderer(template.TemplateRenderer):
'''Data source renderer the expected configuration looks like
this:
{
'name': u'Datas from xyz',
'class': 'cmsplugin_blurp.renderers.data_source.Renderer',
'sources': [
{
'slug': 'slug', # mandatory
'url': 'https://...', # mandatory
'parser_type': 'raw', # optional, possible values are json, xml, css, csv or raw, default value is raw
'content_type': 'application/octet-stream', # optional, default value is compute from the parser_type
'auth_mech': None, # optional, possible values are hmac-sha1, hmac-sha256,oauth2', default is None
'signature_key': None, # mandatory if auth_mech is not None
'verify_certificate': False, # optional, default is False
'allow_redirects': True, # optional default is True
'timeout': 10, # optional default is 1, it cannot be less than 1
'refresh': 3600, # optional, default is taken from the renderer level
'limit': 0, # optional, default is taken from the renderer level
},
]
'template_name': 'data_from_xyz.html'
# default template if the template file cannot be found
'template': '{{ slug|pprint }}'
# time between refresh of the case
# use 0 for no cache
# cache is also update if updatecache key is in the query string
# you can also override it in each source
'refresh': 3600, # optional default is 3600 seconds
# limit to the number of elements to return,
# not limited if it is 0
# you can also override it in each source
'limit': 0, # optional default is 0
}
'''
@classmethod
def check_config(cls, config):
if not 'sources' in config \
or not isinstance(config['sources'], (tuple, list)) \
or len(config['sources']) == 0:
yield 'sources must be a list or a tuple containing at least one element'
for source in config['sources']:
if not 'slug' in source:
yield 'each source must have a slug key'
if not 'url' in source:
yield 'each source must have an url key'
if 'parser_type' in source \
and source['parser_type'] not in ('raw', 'csv', 'json', 'xml', 'rss'):
yield 'unknown parser_type {0!r}'.format(source['parser_type'])
if 'auth_mech' in source:
if source['auth_mech'] not in ('hmac-sha1', 'hmac-sha256', 'oauth2'):
yield 'unknown auth_mech {0!r}'.format(source['auth_mech'])
if source['auth_mech'].startswith('hmac-') \
and ('signature_key' not in source
or not isinstance(source['signature_key'], basestring)):
yield 'missing signature_key string'
def get_sources(self, context):
for source in self.config['sources']:
slug = '{0}.{1}'.format(self.slug, source['slug'])
data = Data(slug, self.config, source, context)
yield source['slug'], data
if settings.TEMPLATE_DEBUG:
yield 'blurp_debug__', '\n'.join(self.debug_content(context))
def has_cached_content(self, context):
for source in self.config['sources']:
slug = '{0}.{1}'.format(self.slug, source['slug'])
data = Data(slug, self.config, source, context)
if not data.has_cached_content():
return False
return True
def debug_content(self, context):
try:
yield u'config: {0}'.format(pprint.pformat(self.config))
except Exception, e:
yield u'config: pformat failed {0!r}'.format(e)
for source in self.config.get('sources', []):
slug = source.get('slug')
if not slug:
continue
try:
yield u'slug {0!r}: {1}'.format(slug,
pprint.pformat(context.get(slug)))
except Exception, e:
yield u'slug {0!r}: pformat failed {1!r}'.format(slug, e)
def render(self, context):
for slug, source in self.get_sources(context):
context[slug] = source
return super(Renderer, self).render(context)
class TemplateSourcesRenderer(Renderer):
"""
Interprets its sources urls as django templates and renders them with
current context
"""
def get_sources(self, context):
for source in self.config['sources']:
slug = '{0}.{1}'.format(self.slug, source['slug'])
source = source.copy()
source['url'] = template.Template(source['url']).render(context)
result = source.get('default', {})
result['data'] = Data(slug, self.config, source, context)
yield source['slug'], result
class DictRenderer(Renderer):
"""
Aggregates all data from the sources into a dict and expose it to the
template with the name of its slug
"""
def render(self, context):
context = super(Renderer, self).render(context)
context[self.slug] = OrderedDict()
for slug, data in self.get_sources(context):
context[self.slug][slug] = data
return context
class DictRendererWithDefault(DictRenderer):
"""
Same as DictRender but each dict item contains data under "data" key and
the config defaults unde "default" key
"""
def get_sources(self, context):
for source in self.config['sources']:
slug = '{0}.{1}'.format(self.slug, source['slug'])
result = source.get('default', {})
result['data'] = Data(slug, self.config, source, context)
yield source['slug'], result
class Data(object):
'''Encapsulate data from a source'''
__CACHE_SENTINEL = object()
JSON = 'application/json'
RSS = 'application/rss+xml'
XML = 'text/xml'
CSV = 'text/csv'
OCTET_STREAM = 'application/octet-stream'
MAPPING = {
'json': JSON,
'rss': RSS,
'xml': XML,
'csv': CSV,
'raw': OCTET_STREAM,
}
def __init__(self, slug, config, source, context):
self.slug = slug
self.context = context
self.request = context.get('request')
self.source = source
self.limit = source.get('limit', config.get('limit', 0))
self.refresh = source.get('refresh', config.get('refresh', 0))
self.url = source['url']
self.verify = source.get('verify_certificate', True)
self.redirects = source.get('allow_redirects', False)
self.async = source.get('async', False)
self.timeout = source.get('timeout', 10)
self.auth_mech = source.get('auth_mech')
self.signature_key = None
if source.get('signature_key'):
self.signature_key = source.get('signature_key').encode('ascii')
self.parser_type = source.get('parser_type', 'raw')
self.content_type = source.get('content_type', self.MAPPING[self.parser_type])
self.user_context = source.get('user_context',
config.get('user_context', self.auth_mech == 'oauth2'))
pre_hash = 'datasource-{self.slug}-{self.url}-{self.limit}-' \
'{self.refresh}-{self.auth_mech}-{self.signature_key}' \
.format(self=self)
# If authentication is used
if self.user_context:
pre_hash += '-%s' % unicode(self.request.user).encode('utf-8')
log.debug('key pre hash value %r', pre_hash)
self.key = hashlib.md5(pre_hash).hexdigest()
self.now = time.time()
self.__content = self.__CACHE_SENTINEL
def get_oauth2_access_token(self):
'''Query django-allauth models to find an access token for this user'''
from allauth.socialaccount.models import SocialToken
user = self.request.user
if user.is_authenticated():
try:
token = SocialToken.objects.get(
account__provider='authentic2',
account__user=user)
log.debug('found access token: %r', token)
return token.token
except SocialToken.DoesNotExist:
log.warning('unable to find a social token for user: %r', user)
return ''
def resolve_http_url(self):
try:
self.final_url = self.url
if self.source.get('auth_mech', '').startswith('hmac'):
# remove the hmac- prefix
hash_algo = self.auth_mech[5:]
self.final_url = signature.sign_url(
self.final_url,
self.signature_key,
algo=hash_algo)
log.debug('getting data source from url %r for renderer %s',
self.final_url, self.slug)
headers = {
'Accept': self.content_type,
}
if self.auth_mech == 'oauth2':
headers['Authorization'] = 'Bearer %s' % self.get_oauth2_access_token()
log.debug('with headers %r', headers)
request = requests.get(
self.final_url,
headers=headers,
verify=self.verify,
allow_redirects=self.redirects,
timeout=self.timeout,
stream=True)
request.raise_for_status()
return request.raw, None
except HTTPError:
error = 'HTTP Error %s when loading URL %s for renderer %r' % (
request.status_code,
self.final_url,
self.slug)
log.warning(error)
except Timeout:
error = 'HTTP Request timeout(%s s) when loading URL ' \
'%s for renderer %s' % (
self.timeout,
self.final_url,
self.slug)
log.warning(error)
except RequestException, e:
error = 'HTTP Request failed when loading URL ' \
'%s for renderer %r: %s' % (
self.final_url,
self.slug, e)
log.warning(error)
return None, error
def resolve_file_url(self):
path = self.url[7:]
try:
return file(path), None
except Exception:
error = 'unable to resolve file URL: %r' % self.url
log.warning(error)
return None, error
def update_content(self):
try:
return self.update_content_real()
except:
log.exception('exception while updating content')
def update_content_real(self):
if self.url.startswith('http'):
stream, error = self.resolve_http_url()
elif self.url.startswith('file:'):
stream, error = self.resolve_file_url()
else:
msg = 'unknown scheme: %r' % self.url
log.error(msg)
if settings.TEMPLATE_DEBUG:
return msg
return
if stream is None:
if settings.TEMPLATE_DEBUG:
return error
return
try:
data = getattr(self, 'parse_'+self.parser_type)(stream)
except Exception:
msg = 'error parsing %s content on %s' % (self.parser_type, self.url)
log.exception(msg)
if settings.TEMPLATE_DEBUG:
return msg
return None
if self.refresh and data is not None:
log.debug('set cache for url %r with key %r', self.url, self.key)
cache.set(self.key, (data, self.now+self.refresh), 86400*12)
if self.key in self.UPDATE_THREADS:
c = self.CONDITIONS.setdefault(self.key, threading.Condition())
with c:
self.UPDATE_THREADS.pop(self.key)
self.CONDITIONS.pop(self.key)
return data
UPDATE_THREADS = {}
CONDITIONS = {}
def has_cached_content(self):
self.__content, until = cache.get(self.key, (self.__CACHE_SENTINEL, None))
return self.__content is not self.__CACHE_SENTINEL
def get_content(self):
if self.__content is not self.__CACHE_SENTINEL:
return self.__content
self.__content, until = cache.get(self.key, (self.__CACHE_SENTINEL, None))
use_cache = self.__content is not self.__CACHE_SENTINEL
if not use_cache:
log.debug('found content in cache for url %r', self.url)
# do not use cache if refresh timeout is 0
use_cache = use_cache and self.refresh > 0
if self.refresh == 0:
log.debug('self refresh is 0, ignoring cache')
# do not use cache if updatecache is present in the query string
use_cache = use_cache and (not self.request or 'updatecache' not in self.request.GET)
if self.request and 'updatecache' in self.request.GET:
log.debug('updatecache in query string, ignoring cache')
if use_cache:
if until < self.now:
# reload cache content asynchronously in a thread
# and return the current content
log.debug('asynchronous update for url %r until: %s < now: %s', self.url, until, self.now)
c = self.CONDITIONS.setdefault(self.key, threading.Condition())
t = threading.Thread(target=self.update_content)
t2 = self.UPDATE_THREADS.setdefault(self.key, t)
if t2 is t: # yeah we are the first to run
with c:
t.start()
c.notify_all() # notify other updating thread that we started
if not self.async:
if not t2 is t:
with c:
while not t2.ident:
c.wait()
t2.join()
else:
log.debug('synchronous update for url %r', self.url)
self.__content = self.update_content()
return self.__content
content = property(get_content)
def parse_json(self, stream):
import json
return json.load(stream)
def parse_rss(self, stream):
result = feedparser.parse(stream.read())
entries = result.entries
entries = sorted(result.entries, key=lambda e: e['updated_parsed'])
result.entries = entries[:self.limit]
return result
def parse_raw(self, stream):
return stream.read()
def parse_xml(self, stream):
return ET.fromstring(stream.read())
def parse_csv(self, stream):
import csv
params = self.source.get('csv_params', {})
encoding = self.source.get('csv_encoding', 'utf-8')
def list_decode(l):
return map(lambda s: s.decode(encoding), l)
def dict_decode(d):
return dict((a, b.decode(encoding)) for a, b in d.iteritems())
if hasattr(stream, 'iter_lines'):
stream = stream.iter_lines()
if 'fieldnames' in params:
reader = csv.DictReader(stream, **params)
decoder = dict_decode
else:
reader = csv.reader(stream, **params)
decoder = list_decode
return list(decoder(e) for e in reader)
def __call__(self):
return self.get_content()
def __iter__(self):
return iter(self())

View File

@ -1,19 +0,0 @@
import logging
log = logging.getLogger(__name__)
from . import data_source
class Renderer(data_source.Renderer):
"""
Aggregates all data from the sources into a list and expose them to the
template with the name of its slug
"""
def render(self, context):
context[self.slug] = []
for slug, data in self.get_sources(context):
try:
context[self.slug].extend(data.content)
except Exception as e:
log.exception("exception occured while extending the list: %s", e)
return super(Renderer, self).render(context)

View File

@ -1,71 +0,0 @@
import datetime
import base64
import hmac
import hashlib
import urllib
import random
import urlparse
'''Simple signature scheme for query strings'''
def sign_url(url, key, algo='sha256', timestamp=None, nonce=None):
parsed = urlparse.urlparse(url)
new_query = sign_query(parsed.query, key, algo, timestamp, nonce)
return urlparse.urlunparse(parsed[:4] + (new_query,) + parsed[5:])
def sign_query(query, key, algo='sha256', timestamp=None, nonce=None):
if timestamp is None:
timestamp = datetime.datetime.utcnow()
timestamp = timestamp.strftime('%Y-%m-%dT%H:%M:%SZ')
if nonce is None:
nonce = hex(random.getrandbits(128))[2:]
new_query = query
if new_query:
new_query += '&'
new_query += urllib.urlencode((
('algo', algo),
('timestamp', timestamp),
('nonce', nonce)))
signature = base64.b64encode(sign_string(new_query, key, algo=algo))
new_query += '&signature=' + urllib.quote(signature)
return new_query
def sign_string(s, key, algo='sha256', timedelta=30):
digestmod = getattr(hashlib, algo)
hash = hmac.HMAC(key, digestmod=digestmod, msg=s)
return hash.digest()
def check_url(url, key, known_nonce=None, timedelta=30):
parsed = urlparse.urlparse(url, 'https')
return check_query(parsed.query, key)
def check_query(query, key, known_nonce=None, timedelta=30):
parsed = urlparse.parse_qs(query)
signature = base64.b64decode(parsed['signature'][0])
algo = parsed['algo'][0]
timestamp = parsed['timestamp'][0]
timestamp = datetime.datetime.strptime(timestamp, '%Y-%m-%dT%H:%M:%SZ')
nonce = parsed['nonce']
unsigned_query = query.split('&signature=')[0]
if known_nonce is not None and known_nonce(nonce):
return False
print 'timedelta', datetime.datetime.utcnow() - timestamp
if abs(datetime.datetime.utcnow() - timestamp) > datetime.timedelta(seconds=timedelta):
return False
return check_string(unsigned_query, signature, key, algo=algo)
def check_string(s, signature, key, algo='sha256'):
# constant time compare
signature2 = sign_string(s, key, algo=algo)
if len(signature2) != len(signature):
return False
res = 0
for a, b in zip(signature, signature2):
res |= ord(a) ^ ord(b)
return res == 0
if __name__ == '__main__':
test_key = '12345'
signed_query = sign_query('NameId=_12345&orig=montpellier', test_key)
assert check_query(signed_query, test_key, timedelta=0) is False
assert check_query(signed_query, test_key) is True

View File

@ -1,31 +0,0 @@
import logging
from sqlalchemy import create_engine
from sqlalchemy.sql import text, bindparam
from sqlalchemy.pool import NullPool
from . import template
log = logging.getLogger(__name__)
class Renderer(template.Renderer):
def __init__(self, *args, **kwargs):
super(Renderer, self).__init__(*args, **kwargs)
self.engine = create_engine(self.config['url'],
poolclass=NullPool, **self.config.get('kwargs', {}))
self.views = self.config['views']
def render(self, context):
for view in self.views:
query = view['query']
bindparams = []
for name, value in view.get('bindparams', {}).iteritems():
param = bindparam(name, value=value)
bindparams.append(param)
sql = text(query, bindparams=bindparams)
result = self.engine.execute(sql)
keys = result.keys()
result = [dict(zip(keys, row)) for row in result.fetchall()]
context[view['slug']] = result
return context

View File

@ -1,8 +0,0 @@
from .template import TemplateRenderer
class Renderer(TemplateRenderer):
'''Directly pass the config object to the template'''
def render(self, context):
context.update(self.config)
return context

View File

@ -1,42 +0,0 @@
import logging
from django.core.exceptions import ImproperlyConfigured
from django.template.loader import get_template
from django.template import Template, TemplateDoesNotExist
from django.conf import settings
from .base import BaseRenderer
log = logging.getLogger(__name__)
class TemplateRenderer(BaseRenderer):
'''Base class providing basic functionalities to resolve the template from
the renderer configuration.
'''
@classmethod
def check_config(cls, config):
super(TemplateRenderer, cls).check_config(config)
if not 'template' in config and not 'template_name' in config:
raise ImproperlyConfigured('{0} configuration is missing a template key: {1!r}'.format(
cls.__name__, config))
def render(self, context):
if settings.TEMPLATE_DEBUG:
context['__blurb'] = self
return context
def render_template(self):
'''First try to get a template by path, then compile the inline
template, and if none of that works show an error message.'''
if 'template_name' in self.config:
try:
template = get_template(self.config['template_name'])
except TemplateDoesNotExist:
log.error('template not found: %r', self.config)
template = 'cmsplugin_blurp/template_not_found.html'
elif 'template' in self.config:
template = Template(self.config['template'])
return template

View File

@ -1 +0,0 @@
{% load i18n %}{% trans "Template not found" %}

View File

@ -1,68 +0,0 @@
import logging
import json
from django import template
from django.conf import settings
from django.utils.html import escape
from django.core.serializers import serialize
from django.core.serializers.json import DjangoJSONEncoder
from django.db.models.query import QuerySet
from django.utils.safestring import mark_safe
from classytags.arguments import Argument
from classytags.core import Options, Tag
from .. import utils
register = template.Library()
# originally copied from django-jsonify(https://bitbucket.org/marltu/django-jsonify/)
# released under a three-clause BSD License by Marius Grigaitis
@register.filter
def jsonify(obj):
if isinstance(obj, QuerySet):
return mark_safe(serialize('json', obj))
return mark_safe(json.dumps(obj, cls=DjangoJSONEncoder))
@register.tag
class RenderBlurp(Tag):
name = 'render_blurp'
options = Options(
Argument('name', resolve=False),
)
def render_tag(self, context, name):
renderer = utils.resolve_renderer(name)
if not renderer:
return ''
template = renderer.render_template()
context = renderer.render(context)
try:
if not hasattr(template, 'render'):
template = template.Template(template)
return template.render(context)
except Exception, e:
logging.getLogger(__name__).exception('error while rendering %s',
renderer)
msg = ''
if settings.TEMPLATE_DEBUG:
msg += 'error while rendering %s: %s' % (renderer, e)
msg = '<pre>%s</pre>' % escape(msg)
return msg
@register.tag
class BlurpNode(Tag):
'''Insert content generated from a blurp block and render inside template'''
name = 'blurp'
options = Options(
Argument('name'),
blocks=[('endblurp', 'nodelist')])
def render_tag(self, context, name, nodelist):
context.push()
utils.insert_blurp_in_context(name, context)
output = self.nodelist.render(context)
context.pop()
return output

View File

@ -1,109 +0,0 @@
import os.path
from django.test import TestCase
from django.test.utils import override_settings
from django.template import Context
from cmsplugin_blurp import utils
BASE_FILE = os.path.abspath(
os.path.join(
os.path.dirname(__file__),
'tests_data'))
CMS_PLUGIN_BLURP_RENDERERS = {
'static': {
'template': 'test.html',
'class': 'cmsplugin_blurp.renderers.static.Renderer',
'content': 'xxx',
},
}
for kind in ('raw', 'json', 'rss', 'xml'):
CMS_PLUGIN_BLURP_RENDERERS[kind] = {
'template': 'test.html',
'class': 'cmsplugin_blurp.renderers.data_source.Renderer',
'sources': [
{
'slug': kind,
'parser_type': kind,
'url': 'file://' + os.path.join(BASE_FILE, kind),
}
]
}
CMS_PLUGIN_BLURP_LIST_RENDERERS = {
'json': {
'template': 'test.html',
'class': 'cmsplugin_blurp.renderers.data_source_list.Renderer',
'sources': [
{
'slug': 'list_data_source1',
'parser_type': 'json',
'url': 'file://' + os.path.join(BASE_FILE, 'json'),
},
{
'slug': 'list_data_source2',
'parser_type': 'json',
'url': 'file://' + os.path.join(BASE_FILE, 'json'),
}
]
}
}
@override_settings(CMS_PLUGIN_BLURP_RENDERERS=CMS_PLUGIN_BLURP_RENDERERS)
class RendererTestCase(TestCase):
def test_choices(self):
self.assertEqual(set(utils.renderers_choices()),
set([('static', 'static'),
('raw', 'raw'),
('json', 'json'),
('rss', 'rss'),
('xml', 'xml'),
]))
def test_static_renderer(self):
r = utils.resolve_renderer('static')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('content'))
self.assertTrue(c.has_key('template'))
self.assertEqual(c['content'], 'xxx')
def test_data_source_renderer_raw(self):
r = utils.resolve_renderer('raw')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('raw'))
self.assertEqual(unicode(c['raw']()), 'xxx')
def test_data_source_renderer_json(self):
r = utils.resolve_renderer('json')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('json'))
self.assertEqual(c['json'](), {'xxx':'yyy'})
def test_data_source_renderer_rss(self):
r = utils.resolve_renderer('rss')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('rss'))
self.assertIn('feed', c['rss']())
def test_data_source_renderer_xml(self):
r = utils.resolve_renderer('xml')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('xml'))
self.assertEqual(c['xml']().tag, 'html')
@override_settings(CMS_PLUGIN_BLURP_RENDERERS=CMS_PLUGIN_BLURP_LIST_RENDERERS)
class DataSourceListRenderTestCase(TestCase):
def test_data_source_list_render_json(self):
r = utils.resolve_renderer('json')
self.assertIsNotNone(r)
c = r.render(Context())
self.assertTrue(c.has_key('json'))
self.assertIsInstance(c['json'], list)

View File

@ -1,3 +0,0 @@
{
"xxx": "yyy"
}

View File

@ -1 +0,0 @@
xxx

View File

@ -1,95 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:areena="http://areena.yle.fi/xsd/areena.xsd" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:itunes="http://www.itunes.com/DTDs/Podcast-1.0.dtd" version="2.0">
<channel>
<title><![CDATA[Människor och minnen]]></title>
<link>http://areena.yle.fi/tv/1658066</link>
<pubDate>Mon, 16 Jun 2014 10:05:00 +0300</pubDate>
<description><![CDATA[Radio Vega sänder en serie kallad Människor och minnen.
I den berättar många människor om dramatiska och intressanta händelser från både sina egna liv och vårt lands historia. Det flesta program är mellan 40-60 år gamla och har legat glömda i radioarkiven.]]></description>
<itunes:image href="http://areena.yle.fi/static/mk/images/areena/series/3318611_886_manniskor_och_minnen_720.jpg"/>
<image>
<url>http://areena.yle.fi/static/mk/images/areena/series/3318611_886_manniskor_och_minnen_65.jpg</url>
<title><![CDATA[Människor och minnen]]></title>
<link>http://areena.yle.fi/tv/1658066</link>
</image>
<managingEditor>(areena.info@yle.fi)</managingEditor>
<webMaster>areena.info@yle.fi</webMaster>
<copyright>YLE Areena</copyright>
<generator>YLE Areena</generator>
<language>fi</language>
<docs>http://blogs.law.harvard.edu/tech/rss</docs>
<ttl>15</ttl>
<itunes:author>areena.info@yle.fi</itunes:author>
<itunes:owner>
<itunes:name>areena.info@yle.fi</itunes:name>
</itunes:owner>
<itunes:summary><![CDATA[Radio Vega sänder en serie kallad Människor och minnen.
I den berättar många människor om dramatiska och intressanta händelser från både sina egna liv och vårt lands historia. Det flesta program är mellan 40-60 år gamla och har legat glömda i radioarkiven.]]></itunes:summary>
<item>
<title><![CDATA[Människor och minnen: Tre män och en velociped]]></title>
<media:credit role="production department">Yle Radio Vega</media:credit>
<areena:tvLicense>false</areena:tvLicense>
<link>http://areena.yle.fi/tv/2231861</link>
<guid isPermaLink="false">09e60bbfad60423cb2cc1906153cba0c</guid>
<description><![CDATA[Inför midsommar och lediga sommardagar kan det vara på sin plats att ge ett litet annorlunda lästips. Författaren Jerome K Jerome uppnådde alla författares dröm, men också mardröm. Han skrev en mycket rolig bok som blev världskänd: Tre män i en båt. Mardrömmen ligger däri att i hans stora produktion av böcker just bara den här är känd - hans andra alster är glömda.
Men han har faktiskt skrivit en mycket roligare bok: Tre män och en velociped från 1900 om tre unga mäns vådliga cykelfärder genom sekelskiftets Europa. Om den kåserade Christer Topelius 1969 och för uppläsningen stod Stig Törnroos.]]></description>
<pubDate>Mon, 16 Jun 2014 11:00:00 +0300</pubDate>
<category>Fakta ja kulttuuri</category>
<enclosure url="http://download.yle.fi/areena/world_download/c4/c4de406d67c34088b937f95274328016.mp3?filename=_20140616_1100.mp3" type="audio/mp3" length="417497"/>
<itunes:image href="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_720.jpg"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_65.jpg" width="65" height="37"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_160.jpg" width="160" height="90"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_220.jpg" width="220" height="124"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_620.jpg" width="620" height="349"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216_720.jpg" width="720" height="405"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/09/09e60bbfad60423cb2cc1906153cba0c/09e60bbfad60423cb2cc1906153cba0c_1402906631216.jpg" width="940" height="529"/>
<dcterms:valid>start=2014-06-16T11:00:00+0300; end=2014-07-16T23:59:59+0300; scheme=W3C-DTF;</dcterms:valid>
<media:content duration="3187"/>
</item>
<item>
<title><![CDATA[Människor och minnen: De 99 minareternas stad]]></title>
<media:credit role="production department">Yle Radio Vega</media:credit>
<areena:tvLicense>false</areena:tvLicense>
<link>http://areena.yle.fi/tv/2270731</link>
<guid isPermaLink="false">68115c4dfece4893b6c7fd43b45233e6</guid>
<description><![CDATA[Den 28 juni 1914, för 100 år sedan small de ödesdigra skotten i Sarajevo när det Habsburgska kejsardömets tronföljare ärkehertig Frans Ferdinand och hans hustru Sophie sköts ihjäl.
Skotten kom också att bli startskottet för det första världskriget.
Förövaren var studenten Gavrilo Princip och i dagens program från 1959, följer vi Pontus Nordling på en vandring genom Sarajevo. Han samtalar bland annat med Gavrilo Princips kusin Bogdan som också var medlem i nationalistgruppen.]]></description>
<pubDate>Mon, 9 Jun 2014 11:00:00 +0300</pubDate>
<category>Fakta ja kulttuuri</category>
<enclosure url="http://download.yle.fi/areena/world_download/b7/b731af13c1504c759776d07ed99f8217.mp3?filename=_20140609_1100.mp3" type="audio/mp3" length="417497"/>
<itunes:image href="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_720.jpg"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_65.jpg" width="65" height="37"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_160.jpg" width="160" height="90"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_220.jpg" width="220" height="124"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_620.jpg" width="620" height="349"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831_720.jpg" width="720" height="405"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/68/68115c4dfece4893b6c7fd43b45233e6/68115c4dfece4893b6c7fd43b45233e6_1402302405831.jpg" width="940" height="529"/>
<dcterms:valid>start=2014-06-09T11:00:00+0300; end=2014-07-09T23:59:59+0300; scheme=W3C-DTF;</dcterms:valid>
<media:content duration="3187"/>
</item>
<item>
<title><![CDATA[Människor och minnen: Författaren och båten]]></title>
<media:credit role="production department">Yle Radio Vega</media:credit>
<areena:tvLicense>false</areena:tvLicense>
<link>http://areena.yle.fi/tv/2266940</link>
<guid isPermaLink="false">4e5da61ca5904810b530ef72551ece02</guid>
<description><![CDATA[Ole Torvalds berättar om sitt förhållande till havet och båtarna. Han var journalist på tidningarna Västra Nyland och på Österbottningen och kom sedan till Åbo och Åbo Underrättelser där han också en tid var chefredaktör. Men Ole Torvalds var också översättare och poet och han har givit ut ett tiotal böcker. Ole Torvalds föddes 1916 och var far till Europaparlamentarikern Nils Torvalds och farfar till Linus Torvalds.
I ett samtal med Aagot Jung för femtio år sedan berättar Ole Torvalds om sitt förhållande till båtar och om minnesvärda seglatser med Björn Landström.]]></description>
<pubDate>Mon, 2 Jun 2014 11:00:00 +0300</pubDate>
<category>Fakta ja kulttuuri</category>
<enclosure url="http://download.yle.fi/areena/world_download/df/df5941fd98f7409599ae584f6fdea0be.mp3?filename=_20140602_1100.mp3" type="audio/mp3" length="417497"/>
<itunes:image href="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_720.jpg"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_65.jpg" width="65" height="37"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_160.jpg" width="160" height="90"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_220.jpg" width="220" height="124"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_620.jpg" width="620" height="349"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624_720.jpg" width="720" height="405"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401696738416.jpg" width="940" height="529"/>
<media:thumbnail url="http://areena.yle.fi/static/mk/images/previews/4e/4e5da61ca5904810b530ef72551ece02/4e5da61ca5904810b530ef72551ece02_1401697018624.jpg" width="940" height="529"/>
<dcterms:valid>start=2014-06-02T11:00:00+0300; end=2014-07-02T23:59:59+0300; scheme=W3C-DTF;</dcterms:valid>
<media:content duration="3187"/>
</item>
</channel>
</rss>

View File

@ -1 +0,0 @@
<html></html>

View File

@ -1,39 +0,0 @@
try:
from importlib import import_module
except ImportError:
from django.utils.importlib import import_module
from django.utils.translation import ugettext_lazy as _
from . import app_settings
def renderer_description(renderer):
if 'name' in renderer.config:
if 'template_name' in renderer.config:
return _('{name} using template {template}').format(
name=renderer.config['name'],
template=renderer.config['template_name'])
else:
return renderer.config['name']
def renderers_choices():
for slug in app_settings.RENDERERS:
renderer = resolve_renderer(slug)
yield slug, renderer_description(renderer) or slug
def create_renderer(name, instance):
'''Create a renderer instance of given name from a settings dictionary'''
module_name, class_name = instance['class'].rsplit('.', 1)
module = import_module(module_name)
return getattr(module, class_name)(name, instance)
def resolve_renderer(name):
'''Create a renderer instance from slug name of its settings'''
instance = app_settings.RENDERERS.get(name)
if instance:
return create_renderer(name, instance)
def insert_blurp_in_context(name, context):
renderer = resolve_renderer(name)
if not renderer:
return ''
return renderer.render(context)