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:
parent
a6f39bc74a
commit
5df2055a17
@ -72,6 +72,7 @@ the following topic guides.
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
topics/workflows
|
||||
topics/tables
|
||||
topics/policy
|
||||
topics/testing
|
||||
|
@ -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
|
||||
=========
|
||||
|
||||
|
@ -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
|
||||
=========
|
||||
|
||||
|
@ -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)
|
||||
|
134
doc/source/topics/workflows.rst
Normal file
134
doc/source/topics/workflows.rst
Normal 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.
|
Loading…
x
Reference in New Issue
Block a user