From 027e3a230206b0cb75d43554b2663dd42b2805da Mon Sep 17 00:00:00 2001 From: Jeremy Cline Date: Feb 03 2017 20:27:45 +0000 Subject: Document Fedora Hubs Widgets Widgets in Hubs have an interface they are expected to implement. This formally defines and documents this interface as Sphinx docblocks in code. The docblocks are then rendered to HTML using the Sphinx autodoc plugin as part of a new API section of the documentation project. This replaces the separate API documentation for widgets originally found in the dev guide. This has the advantage of creating a single source of truth for API documentation. This commit does introduce a new ``Widget`` class to define the interface. It should function in the same way as the widgets currently defined at the module level and truthfully only serves to bundle the interface into one neat object within the module. Signed-off-by: Jeremy Cline --- diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..093ff4e --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,13 @@ +Developer Interfaces +==================== + +This documents ways developers can interface with Fedora Hubs. + +.. _widgets-api: + +Widgets +------- + +.. automodule:: hubs.widgets + :members: + :show-inheritance: diff --git a/docs/conf.py b/docs/conf.py index ca55b4f..cb0f636 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,20 +16,25 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('../')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # -# needs_sphinx = '1.0' +needs_sphinx = '1.3' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. -extensions = [] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', + 'sphinx.ext.napoleon', + 'sphinx.ext.viewcode', +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] diff --git a/docs/dev-guide.rst b/docs/dev-guide.rst index 2617a29..7d544ca 100644 --- a/docs/dev-guide.rst +++ b/docs/dev-guide.rst @@ -344,31 +344,10 @@ displayed). Stubbing out a new Widget ========================= -You write a new widget in the ``hubs/widgets/`` directory and must declare it -in the registry dict in ``hubs/widgets/__init__.py``. - -In order to be valid, a widget must have: - -- A ``data(session, widgets, **kwargs)`` function that returns a - jsonifiable dict of data. This will get cached -- more on that later. -- A ``template`` object that is a jinja2 template for that widget. -- Optionally, a ``chrome`` decorator. -- A ``should_invalidate(message, session, widget)`` function that will be used to - *potentially* invalidate the widget's cache. That function will get called by - a backend daemon listening for fedmsg messages so when you update your group - memberships in FAS, a fedmsg message hits the fedora-hubs backend and returns - True if the lookup value should be nuked/refreshed in memcached (or some - other store). - -If you want to try making a new widget: - -- Copy an existing one in ``hubs/widgets/`` -- Add it to the registry in ``hubs/widgets/__init__.py`` -- If you want it to show up on a **user** page, add it to ``hubs/defaults.py`` - in the ``add_user_widgets`` function. -- If you want it to show up on **group** pages, add it to ``populate.py``. - -Destroy your database, rebuild it, and re-run the app. Your widget should show up. +Widgets can be added and removed from a Hub to provide a customized experience +for each user. To learn how to implement a new widget, consult the +:ref:`widgets-api` documentation. + Widget-specific views --------------------- diff --git a/docs/index.rst b/docs/index.rst index 1f5379f..72ec92e 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,6 +13,7 @@ Contents: overview dev-guide + api Indices and tables diff --git a/hubs/widgets/__init__.py b/hubs/widgets/__init__.py index 95253e7..b8f0956 100755 --- a/hubs/widgets/__init__.py +++ b/hubs/widgets/__init__.py @@ -1,3 +1,42 @@ +# -*- coding: utf-8 -*- +# This file is part of Fedora Hubs. +# +# Copyright (C) 2015 Dhriti Shikhar +# Copyright (C) 2015 Ralph Bean +# Copyright (C) 2015 Remy DeCausemaker +# Copyright (C) 2016 Pierre-Yves Chibon +# Copyright (C) 2017 Jeremy Cline +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +""" +Widgets are the reusable building blocks of Fedora Hubs. + +To implement a new widget, add a new module in :mod:`hubs.widgets` package that +implements the interface defined in :class:`hubs.widgets.Widget`. Then, add +that module to the :data:`hubs.widgets.registry` dictionary. + +If you would like it to show up in a user hub by default, add it to +:mod:`hubs.defaults`. + +If you wish to add it to a group page in the development environment, add your +new widget to the ``populate.py`` script in the repository root. Next destroy +your database, rebuild it, and re-run the app. Your widget should show up. + +Attributes: + registry (dict): This dictionary is a registry of available widgets in + Fedora Hubs. +""" from __future__ import unicode_literals from hubs.widgets import dummy @@ -120,7 +159,14 @@ def get_site_vars(): def render(module, session, widget, *args, **kwargs): """ Main API entry point. - Call this to render a widget into HTML + Call this to render a widget into HTML. + + Args: + module: An object that implements the Widget interface + session (sqlalchemy.orm.scoping.scoped_session): An SQLAlchemy session + to use when querying the database. + widget (hubs.models.Widget): The Widget instance to render + """ # The API returns exactly this data. Shared cache data = module.data(session, widget, *args, **kwargs) @@ -130,3 +176,76 @@ def render(module, session, widget, *args, **kwargs): # Use the API data to fill out a template, and potentially decorate it. return module.render(**data) + + +class Widget(object): + """ + This is the widget interface all widgets must implement. + + In order to implement a new widget in Fedora Hubs, you should subclass this + class, implement its methods, and set the required class attributes. You + must then register the :class:`Widget` in the Widget registry. The widget + registry is a simple dictionary found at :func:`hubs.widgets.registry`. + + Attributes: + template (jinja2.Template): The `Jinja2 template`_ for this widget. + position (str): The position of the widget in the rendered page. It + should be one of the following values: 'left', 'right', or 'both'. + render (callable): This attribute is set to ``template.render`` + automatically and should be a callable that returns some HTML for + this widget. + + .. _Jinja2 template: + http://jinja.pocoo.org/docs/latest/api/#jinja2.Template + """ + + template = None + position = None + render = None if template is None else template.render + + @staticmethod + def data(session, widget, *args, **kwargs): + """ + This generates the data used to render a widget. + + The result of this function is cached and this function will not be + called again until the :meth:`Widget.should_invalidate` returns + `True`. + + Args: + module: An object that implements the Widget interface + session (sqlalchemy.orm.scoping.scoped_session): An SQLAlchemy + session to use when querying the database. + widget (hubs.models.Widget): The Widget instance to render + + Returns: + dict: A JSON-serializable dictionary of data. + + This data will be cached. + """ + raise NotImplementedError() + + @staticmethod + def should_invalidate(message, session, widget): + """ + A function that is used to determine when to invalidate the widget's + cache. + + This function will be called when a fedmsg arrives. The widget should + use this to decide whether it needs to regenerate its data. + + Args: + message (dict): The fedmsg that triggered this function call. + session (sqlalchemy.orm.scoping.scoped_session): An SQLAlchemy + session to use when querying the database. + widget (hubs.models.Widget): The Widget instance to render + + Returns: + bool: True if the cache needs to be rebuilt, False otherwise + + A widget should return `False` if the message received would not + change the result of a call it the :meth:`Widget.data` function. + When it returns `True`, this widget's :meth:`Widget.data` function + will be called to refresh the cache. + """ + raise NotImplementedError()