Adding documentation to Tables and Workflows

Adding documentation of DataTable decorators.
Adding documentation and example of complex workflow.

Connecting topics with api reference.

Change-Id: I2653e0c5c52179dcc45cee689cde50afcd6ae0de
Fixes: bug #1212559
This commit is contained in:
Ladislav Smola 2013-08-15 11:26:19 +02:00
parent a6f39bc74a
commit 5df2055a17
5 changed files with 237 additions and 0 deletions

View File

@ -72,6 +72,7 @@ the following topic guides.
.. toctree::
:maxdepth: 1
topics/workflows
topics/tables
topics/policy
topics/testing

View File

@ -10,6 +10,11 @@ correctly and consistently, table- and row-level actions all have a consistent
API and appearance, and generally you don't have to reinvent the wheel or
copy-and-paste every time you need a new table!
.. seealso::
For usage information, tips & tricks and more examples check out the :doc:`DataTables Topic
Guide </topics/tables>`.
DataTable
=========

View File

@ -8,6 +8,11 @@ One of the most challenging aspects of building a compelling user experience
is crafting complex multi-part workflows. Horizon's ``workflows`` module
aims to bring that capability within everyday reach.
.. seealso::
For usage information, tips & tricks and more examples check out the
:doc:`Workflows Topic Guide </topics/workflows>`.
Workflows
=========

View File

@ -154,3 +154,95 @@ displayed solely based on the result of
For more information on policy based Role Based Access Control see:
:doc:`Horizon Policy Enforcement (RBAC: Role Based Access Control) </topics/policy>`.
Table Cell filters (decorators)
===============================
DataTable displays lists of objects in rows and object attributes in cell.
How should we proceed, if we want to decorate some column, e.g. if we have
column ``memory`` which returns a number e.g. 1024, and we want to show
something like 1024.00 GB inside table?
Decorator pattern
-----------------
The clear anti-pattern is defining the new attributes on object like
``ram_float_format_2_gb`` or to tweak a DataTable in any way for displaying
purposes.
The cleanest way is to use ``filters``. Filters are decorators, following GOF
``Decorator pattern``. This way ``DataTable logic`` and ``displayed object
logic`` are correctly separated from ``presentation logic`` of the object
inside of the various tables. And therefore the filters are reusable in all
tables.
Filter function
---------------
Horizon DatablesTable takes a tuple of pointers to filter functions
or anonymous lambda functions. When displaying a ``Cell``, ``DataTable``
takes ``Column`` filter functions from left to right, using the returned value
of the previous function as a parameter of the following function. Then
displaying the returned value of the last filter function.
A valid filter function takes one parameter and returns the decorated value.
So e.g. these are valid filter functions ::
# Filter function.
def add_unit(v):
return str(v) + " GB"
# Or filter lambda function.
lambda v: str(v) + " GB"
# This is also a valid definition of course, although for the change of the
# unit parameter, function has to be wrapped by lambda
# (e.g. floatformat function example below).
def add_unit(v, unit="GB"):
return str(v) + " " + unit
Using filters in DataTable column
---------------------------------
DataTable takes tuple of filter functions, so e.g. this is valid decorating
of a value with float format and with unit ::
ram = tables.Column(
"ram",
verbose_name=_('Memory'),
filters=(lambda v: floatformat(v, 2),
add_unit))
It always takes tuple, so using only one filter would look like this ::
filters=(lambda v: floatformat(v, 2),)
The decorated parameter doesn't have to be only a string or number, it can
be anything e.g. list or an object. So decorating of object, that has
attributes value and unit would look like this ::
ram = tables.Column(
"ram",
verbose_name=_('Memory'),
filters=(lambda x: getattr(x, 'value', '') +
" " + getattr(x, 'unit', ''),))
Available filters
-----------------
There are a load of filters, that can be used, defined in django already:
https://github.com/django/django/blob/master/django/template/defaultfilters.py
So it's enough to just import and use them, e.g. ::
from django.template import defaultfilters as filters
# code omitted
filters=(filters.yesno, filters.capfirst)
from django.template.defaultfilters import timesince
from django.template.defaultfilters import title
# code omitted
filters=(parse_isotime, timesince)

View File

@ -0,0 +1,134 @@
======================
Workflows Topic Guide
======================
One of the most challenging aspects of building a compelling user experience
is crafting complex multi-part workflows. Horizon's ``workflows`` module
aims to bring that capability within everyday reach.
.. seealso::
For a detailed API information check out the :doc:`Workflows Reference
Guide </ref/workflows>`.
Workflows
=========
Workflows are complex forms with tabs, each workflow must consist of classes
extending the :class:`~horizon.workflows.Workflow`,
:class:`~horizon.workflows.Step` and :class:`~horizon.workflows.Action`
Complex example of workflow
----------------------------
The following is a complex example of how data are exchanged between
urls, views, workflows and templates:
#. In ``urls.py``, we have the named parameter. E.g. ``resource_class_id``. ::
RESOURCE_CLASS = r'^(?P<resource_class_id>[^/]+)/%s$'
urlpatterns = patterns(
'',
url(RESOURCE_CLASS % 'update', UpdateView.as_view(), name='update'))
#. In ``views.py``, we pass data to the template and to the action(form)
(action can also pass data to the ``get_context_data`` method and to the
template). ::
class UpdateView(workflows.WorkflowView):
workflow_class = UpdateResourceClass
def get_context_data(self, **kwargs):
context = super(UpdateView, self).get_context_data(**kwargs)
# Data from URL are always in self.kwargs, here we pass the data
# to the template.
context["resource_class_id"] = self.kwargs['resource_class_id']
# Data contributed by Workflow's Steps are in the
# context['workflow'].context list. We can use that in the
# template too.
return context
def _get_object(self, *args, **kwargs):
# Data from URL are always in self.kwargs, we can use them here
# to load our object of interest.
resource_class_id = self.kwargs['resource_class_id']
# Code omitted, this method should return some object obtained
# from API.
def get_initial(self):
resource_class = self._get_object()
# This data will be available in the Action's methods and
# Workflow's handle method.
# But only if the steps will depend on them.
return {'resource_class_id': resource_class.id,
'name': resource_class.name,
'service_type': resource_class.service_type}
#. In ``workflows.py`` we process the data, it is just more complex django
form. ::
class ResourcesAction(workflows.Action):
# The name field will be automatically available in all action's
# methods.
# If we want this field to be used in the another Step or Workflow,
# it has to be contributed by this step, then depend on in another
# step.
name = forms.CharField(max_length=255,
label=_("Testing Name"),
help_text="",
required=True)
def handle(self, request, data):
pass
# If we want to use some data from the URL, the Action's step
# has to depend on them. It's then available in
# self.initial['resource_class_id'] or data['resource_class_id'].
# In other words, resource_class_id has to be passed by view's
# get_initial and listed in step's depends_on list.
# We can also use here the data from the other steps. If we want
# the data from the other step, the step needs to contribute the
# data and the steps needs to be ordered properly.
class UpdateResources(workflows.Step):
# This passes data from Workflow context to action methods
# (handle, clean). Workflow context consists of URL data and data
# contributed by other steps.
depends_on = ("resource_class_id",)
# By contributing, the data on these indexes will become available to
# Workflow and to other Steps (if they will depend on them). Notice,
# that the resources_object_ids key has to be manually added in
# contribute method first.
contributes = ("resources_object_ids", "name")
def contribute(self, data, context):
# We can obtain the http request from workflow.
request = self.workflow.request
if data:
# Only fields defined in Action are automatically
# available for contribution. If we want to contribute
# something else, We need to override the contribute method
# and manually add it to the dictionary.
context["resources_object_ids"] =\
request.POST.getlist("resources_object_ids")
# We have to merge new context with the passed data or let
# the superclass do this.
context.update(data)
return context
class UpdateResourceClass(workflows.Workflow):
default_steps = (UpdateResources,)
def handle(self, request, data):
pass
# This method is called as last (after all Action's handle
# methods). All data that are listed in step's 'contributes='
# and 'depends_on=' are available here.
# It can be easier to have the saving logic only here if steps
# are heavily connected or complex.
# data["resources_object_ids"], data["name"] and
# data["resources_class_id"] are available here.