| |
@@ -1,117 +1,201 @@
|
| |
- from __future__ import unicode_literals
|
| |
-
|
| |
- import collections
|
| |
- import datetime
|
| |
- import functools
|
| |
- import hashlib
|
| |
- import json
|
| |
- import sys
|
| |
-
|
| |
- import dogpile.cache
|
| |
- import flask
|
| |
- import six.moves.urllib_parse
|
| |
-
|
| |
- import fedmsg.config
|
| |
+ from __future__ import unicode_literals, absolute_import
|
| |
|
| |
import logging
|
| |
- log = logging.getLogger(__name__)
|
| |
-
|
| |
- config = fedmsg.config.load_config()
|
| |
- cache_defaults = {
|
| |
- "backend": "dogpile.cache.dbm",
|
| |
- "expiration_time": 1, # Expire every 1 second, for development
|
| |
- "arguments": {
|
| |
- "filename": "/var/tmp/fedora-hubs-cache.db",
|
| |
- },
|
| |
- }
|
| |
- cache = dogpile.cache.make_region()
|
| |
- cache.configure(**config.get('fedora-hubs.cache', cache_defaults))
|
| |
-
|
| |
-
|
| |
- Argument = collections.namedtuple(
|
| |
- 'Argument', ('name', 'default', 'validator', 'help'))
|
| |
-
|
| |
-
|
| |
- def argument(name, default, validator, help):
|
| |
- def decorator(func):
|
| |
- @wraps(func)
|
| |
- def inner(*args, **kwargs):
|
| |
- return func(*args, **kwargs)
|
| |
-
|
| |
- inner.widget_arguments.append(Argument(name, default, validator, help))
|
| |
- return inner
|
| |
- return decorator
|
| |
-
|
| |
-
|
| |
- def AGPLv3(name):
|
| |
- def decorator(func):
|
| |
- @wraps(func)
|
| |
- def inner(session, widget, *args, **kwargs):
|
| |
- result = func(session, widget, *args, **kwargs)
|
| |
- result['source_url'] = flask.url_for('widget_source', name=name)
|
| |
- result['widget_url'] = flask.url_for(
|
| |
- 'widget_render', hub=widget.hub.name, idx=widget.idx)
|
| |
- result['edit_url'] = flask.url_for(
|
| |
- 'widget_edit', hub=widget.hub.name, idx=widget.idx)
|
| |
- result['widget'] = widget
|
| |
- return result
|
| |
-
|
| |
- return inner
|
| |
- return decorator
|
| |
-
|
| |
-
|
| |
- def smartcache(func):
|
| |
- @wraps(func)
|
| |
- def inner(session, widget, *args, **kwargs):
|
| |
- key = cache_key_generator(widget, *args, **kwargs)
|
| |
- creator = lambda: func(session, widget, *args, **kwargs)
|
| |
- #log.debug("Accessing cache key %s", key)
|
| |
- return cache.get_or_create(key, creator)
|
| |
-
|
| |
- return inner
|
| |
-
|
| |
-
|
| |
- def invalidate_cache(widget, *args, **kwargs):
|
| |
- key = cache_key_generator(widget, *args, **kwargs)
|
| |
+ import re
|
| |
+ import flask
|
| |
+ import jinja2
|
| |
+ import six
|
| |
|
| |
- value = cache.get(key, ignore_expiration=True)
|
| |
- if isinstance(value, dogpile.cache.api.NoValue):
|
| |
- log.debug("Not deleting cache key %s. It is absent.", key)
|
| |
- return
|
| |
+ from importlib import import_module
|
| |
+ from .caching import CachedFunction
|
| |
+ from .view import WidgetView
|
| |
|
| |
- widget.hub.last_refreshed = datetime.datetime.utcnow()
|
| |
- log.debug("Deleting cache key %s.", key)
|
| |
- cache.delete(key)
|
| |
|
| |
+ log = logging.getLogger(__name__)
|
| |
|
| |
- def cache_key_generator(widget, *args, **kwargs):
|
| |
- return "|".join([
|
| |
- str(widget.idx),
|
| |
- json.dumps(args),
|
| |
- json.dumps(kwargs),
|
| |
- ]).encode('utf-8')
|
| |
|
| |
+ class WidgetParameter(object):
|
| |
+ """
|
| |
+ Configuration option for a widget.
|
| |
+
|
| |
+ A widget can be configured differently for each instance of this widget in
|
| |
+ a hub. The list of configuration options is described by the list of
|
| |
+ :py:class:`WidgetParameter` objects returned by the widget's
|
| |
+ :py:meth:`~Widget.get_parameters` method.
|
| |
+
|
| |
+ Attributes:
|
| |
+ name (str): The name of the parameter.
|
| |
+ label (str): A humanized name of the parameter, which will be shown in
|
| |
+ the UI.
|
| |
+ default: The default value if this parameter is not set.
|
| |
+ validator (function): A validation function that will be called when
|
| |
+ a user sets the parameter value.
|
| |
+ help (str): A help text that will be shown in the UI.
|
| |
+ """
|
| |
|
| |
- def wraps(original):
|
| |
- @functools.wraps(original)
|
| |
- def decorator(subsequent):
|
| |
- subsequent = functools.wraps(original)(subsequent)
|
| |
- subsequent.widget_arguments = getattr(original, 'widget_arguments', [])
|
| |
- return subsequent
|
| |
- return decorator
|
| |
+ _attrs = ('name', 'label', 'default', 'validator', 'help')
|
| |
|
| |
+ def __init__(self, **kwargs):
|
| |
+ for name in self._attrs:
|
| |
+ setattr(self, name, kwargs.pop(name, None))
|
| |
+ for name in kwargs:
|
| |
+ raise TypeError("Invalid attribute: %s" % name)
|
| |
|
| |
- def widget_route(**options):
|
| |
- """Register a view for the current widget.
|
| |
|
| |
- This decorator can be used to expose a specific function below a widget's
|
| |
- URL endpoint. Refer to the "Widget-specific views" section in the
|
| |
- documentation to learn how to construct the corresponding URL.
|
| |
+ class Widget(object):
|
| |
+ """
|
| |
+ The main widget class, you must subclass it to create a widget.
|
| |
+
|
| |
+ Arguments:
|
| |
+ name (str): The widget name. It will not be displayed in the UI, but
|
| |
+ will appear in some URLs, so be careful to only use simple,
|
| |
+ URL-compatible characters.
|
| |
+ position (str): The position of the widget in the rendered page. It
|
| |
+ should be one of the following values: ``left``, ``right``, or
|
| |
+ ``both``.
|
| |
+ parameters (list): A list of dictionaries that describe a widget's
|
| |
+ configuration. See :py:class:`hubs.widgets.base.WidgetParameter`.
|
| |
+ views_module (list): The Python path to the module where the widget
|
| |
+ views must be looked for. By default, they are looked for in the
|
| |
+ same module.
|
| |
+ cached_functions_module (list): The Python path to the module where the
|
| |
+ cached functions must be looked for. By default, they are looked
|
| |
+ for in the same module.
|
| |
"""
|
| |
- def decorator(func):
|
| |
- options["view_func_name"] = func.__name__
|
| |
- mdict = sys.modules[func.__module__].__dict__
|
| |
- mroutes = mdict.setdefault('ROUTES', [])
|
| |
- mroutes.append(options)
|
| |
- return func
|
| |
- return decorator
|
| |
+
|
| |
+ name = None
|
| |
+ position = None
|
| |
+ parameters = []
|
| |
+ views_module = None
|
| |
+ cached_functions_module = None
|
| |
+
|
| |
+ def __init__(self):
|
| |
+ self._template_environment = None
|
| |
+
|
| |
+ def validate(self):
|
| |
+ """
|
| |
+ Ensure that the widget has the bits it needs.
|
| |
+
|
| |
+ Raises: AttributeError
|
| |
+ """
|
| |
+ if self.name is None:
|
| |
+ raise AttributeError('widgets must have a "name" attribute')
|
| |
+ if not re.match('^[\w_.-]+$', self.name):
|
| |
+ raise AttributeError(
|
| |
+ 'invalid widget name: %r. ' % self.name +
|
| |
+ 'Please use URL-compatible characters.')
|
| |
+ if self.position not in ['left', 'right', 'both']:
|
| |
+ raise AttributeError(
|
| |
+ '"position" attribute is not: `left`, `right` or `both`'
|
| |
+ )
|
| |
+ root_view = self.get_views().get("root")
|
| |
+ if root_view is None:
|
| |
+ raise AttributeError(
|
| |
+ 'widgets must have a "root" view, please refer to the '
|
| |
+ 'documentation.')
|
| |
+ if "/" not in root_view.url_rules:
|
| |
+ raise AttributeError(
|
| |
+ 'the root view must be registered for the "/" URL rule.')
|
| |
+
|
| |
+ def get_parameters(self):
|
| |
+ """
|
| |
+ Return the :py:class:`WidgetParameter` instances that describe this
|
| |
+ widget's configuration options.
|
| |
+ """
|
| |
+ return [WidgetParameter(**param) for param in self.parameters]
|
| |
+
|
| |
+ def get_template_environment(self):
|
| |
+ """
|
| |
+ Get the template environment that widget-specific views will use.
|
| |
+
|
| |
+ If additional filters or variables are needed in the templates, they
|
| |
+ can be added by overriding this method in the widget subclass.
|
| |
+ """
|
| |
+ if self._template_environment is None:
|
| |
+ env = jinja2.Environment(
|
| |
+ loader=jinja2.ChoiceLoader([
|
| |
+ jinja2.PackageLoader(self.__module__, "templates"),
|
| |
+ jinja2.PackageLoader("hubs.widgets", "templates"),
|
| |
+ ]),
|
| |
+ trim_blocks=True,
|
| |
+ )
|
| |
+ env.filters['tojson'] = flask.json.tojson_filter
|
| |
+ env.globals.update({
|
| |
+ 'session': flask.app.session,
|
| |
+ 'g': flask.g,
|
| |
+ 'url_for': flask.url_for,
|
| |
+ })
|
| |
+ self._template_environment = env
|
| |
+ return self._template_environment
|
| |
+
|
| |
+ def _get_local_subclasses(self, parent_class, module=None):
|
| |
+ """
|
| |
+ Returns the subclasses of the ``parent_class`` defined in this widget's
|
| |
+ Python module.
|
| |
+ """
|
| |
+ if module is None:
|
| |
+ module = self.__module__
|
| |
+ result = []
|
| |
+ try:
|
| |
+ widget_module = import_module(module, package=self.__module__)
|
| |
+ except ImportError as e:
|
| |
+ raise AttributeError(
|
| |
+ "Could not find subclasses of %s in %s: %s"
|
| |
+ % (parent_class, module, e))
|
| |
+ for objname in dir(widget_module):
|
| |
+ obj = getattr(widget_module, objname)
|
| |
+ if type(obj) != type(object):
|
| |
+ continue # we only look for classes
|
| |
+ if not issubclass(obj, parent_class):
|
| |
+ continue # we only want subclasses of the parent_class
|
| |
+ if obj.__module__ != widget_module.__name__:
|
| |
+ # we don't want locally imported classes (like the
|
| |
+ # parent_class itself)
|
| |
+ continue
|
| |
+ result.append(obj)
|
| |
+ return result
|
| |
+
|
| |
+ def get_views(self):
|
| |
+ """
|
| |
+ Returns:
|
| |
+ dict: A dictionary of the widget-specific views, indexed by their
|
| |
+ :py:attr:`~hubs.widgets.view.WidgetView.name` attribute.
|
| |
+ """
|
| |
+ return dict([
|
| |
+ (view.name, view) for view in
|
| |
+ self._get_local_subclasses(WidgetView, self.views_module)
|
| |
+ ])
|
| |
+
|
| |
+ def register_routes(self, app):
|
| |
+ """
|
| |
+ Register the widget-specific views in the web framework.
|
| |
+
|
| |
+ Args:
|
| |
+ app (flask.Flask): The Flask application to register the views
|
| |
+ with.
|
| |
+
|
| |
+ This function is Flask-specific.
|
| |
+ """
|
| |
+ for view_name, view_class in self.get_views().items():
|
| |
+ endpoint = "%s_%s" % (self.name, view_name)
|
| |
+ if six.PY2:
|
| |
+ # Flask will set the __name__ property, which must be a
|
| |
+ # bytestring on PY2
|
| |
+ endpoint = endpoint.encode("ascii", "replace")
|
| |
+ view_func = view_class.as_view(endpoint, self)
|
| |
+ for url_rule in view_class.url_rules:
|
| |
+ rule = "/<hub>/w/%s/<int:idx>/%s" % (
|
| |
+ self.name, url_rule.lstrip("/"))
|
| |
+ app.add_url_rule(rule, view_func=view_func)
|
| |
+
|
| |
+ def get_cached_functions(self):
|
| |
+ """
|
| |
+ Returns:
|
| |
+ dict: A dictionary of the widget's cached functions, indexed by
|
| |
+ their class name.
|
| |
+ """
|
| |
+ result = {}
|
| |
+ for fn_class in self._get_local_subclasses(
|
| |
+ CachedFunction, self.cached_functions_module):
|
| |
+ result[fn_class.__name__] = fn_class
|
| |
+ return result
|
| |
This does not look right.