Fix H405 (multi line docstring) warnings (horizon)

H405: multi line docstring summary not separated with an empty line

Partial-Bug: #1696996
Change-Id: I58f71206def1a25f3eda04b9297f1aa7d3caa646
This commit is contained in:
Akihiro Motoki 2017-06-09 16:02:30 +00:00
parent 8ecb9c141b
commit 95629a337e
35 changed files with 480 additions and 358 deletions

View File

@ -471,14 +471,13 @@ class Dashboard(Registry, HorizonComponent):
self._panel_groups = None self._panel_groups = None
def get_panel(self, panel): def get_panel(self, panel):
"""Returns the specified :class:`~horizon.Panel` instance registered """Returns the Panel instance registered with this dashboard."""
with this dashboard.
"""
return self._registered(panel) return self._registered(panel)
def get_panels(self): def get_panels(self):
"""Returns the :class:`~horizon.Panel` instances registered with this """Returns the Panel instances registered with this dashboard in order.
dashboard in order, without any panel groupings.
Panel grouping information is not included.
""" """
all_panels = [] all_panels = []
panel_groups = self.get_panel_groups() panel_groups = self.get_panel_groups()
@ -487,8 +486,9 @@ class Dashboard(Registry, HorizonComponent):
return all_panels return all_panels
def get_panel_group(self, slug): def get_panel_group(self, slug):
"""Returns the specified :class:~horizon.PanelGroup """Returns the specified :class:~horizon.PanelGroup.
or None if not registered
Returns None if not registered.
""" """
return self._panel_groups.get(slug) return self._panel_groups.get(slug)
@ -1006,8 +1006,10 @@ class Site(Registry, HorizonComponent):
class HorizonSite(Site): class HorizonSite(Site):
"""A singleton implementation of Site such that all dealings with horizon """A singleton implementation of Site.
get the same instance no matter what. There can be only one.
All dealings with horizon get the same instance no matter what.
There can be only one.
""" """
_instance = None _instance = None

View File

@ -119,8 +119,11 @@ class ResourceBrowser(html.HTMLElement):
% (attr_name, self.__class__.__name__)) % (attr_name, self.__class__.__name__))
def set_tables(self, tables): def set_tables(self, tables):
"""Sets the table instances on the browser from a dictionary mapping """Sets the table instances on the browser.
table names to table instances (as constructed by MultiTableView).
``tables`` argument specifies tables to be set.
It is a dictionary mapping table names to table instances
(as constructed by MultiTableView).
""" """
self.navigation_table = tables[self.navigation_table_class._meta.name] self.navigation_table = tables[self.navigation_table_class._meta.name]
self.content_table = tables[self.content_table_class._meta.name] self.content_table = tables[self.content_table_class._meta.name]

View File

@ -19,8 +19,7 @@ from django.contrib.staticfiles.finders import AppDirectoriesFinder
class HorizonStaticFinder(AppDirectoriesFinder): class HorizonStaticFinder(AppDirectoriesFinder):
"""A static files finder that also looks into the directory of each panel. """Static files finder that also looks into the directory of each panel."""
"""
def __init__(self, app_names=None, *args, **kwargs): def __init__(self, app_names=None, *args, **kwargs):
super(HorizonStaticFinder, self).__init__(*args, **kwargs) super(HorizonStaticFinder, self).__init__(*args, **kwargs)

View File

@ -45,8 +45,10 @@ class HorizonException(Exception):
class Http302(HorizonException): class Http302(HorizonException):
"""Error class which can be raised from within a handler to cause an """Exception used to redirect at the middleware level.
early bailout and redirect at the middleware level.
This error class which can be raised from within a handler to cause
an early bailout and redirect at the middleware level.
""" """
status_code = 302 status_code = 302
@ -56,7 +58,9 @@ class Http302(HorizonException):
class NotAuthorized(HorizonException): class NotAuthorized(HorizonException):
"""Raised whenever a user attempts to access a resource which they do not """User tries to access a resource without sufficient permissions.
Raised whenever a user attempts to access a resource which they do not
have permission-based access to (such as when failing the have permission-based access to (such as when failing the
:func:`~horizon.decorators.require_perms` decorator). :func:`~horizon.decorators.require_perms` decorator).
@ -68,8 +72,7 @@ class NotAuthorized(HorizonException):
class NotAuthenticated(HorizonException): class NotAuthenticated(HorizonException):
"""Raised when a user is trying to make requests and they are not logged """Raised when a user is trying to make requests and they are not logged in.
in.
The included :class:`~horizon.middleware.HorizonMiddleware` catches The included :class:`~horizon.middleware.HorizonMiddleware` catches
``NotAuthenticated`` and handles it gracefully by displaying an error ``NotAuthenticated`` and handles it gracefully by displaying an error
@ -99,8 +102,9 @@ class RecoverableError(HorizonException):
class ServiceCatalogException(HorizonException): class ServiceCatalogException(HorizonException):
"""Raised when a requested service is not available in the """A requested service is not available in the ``ServiceCatalog``.
``ServiceCatalog`` returned by Keystone.
``ServiceCatalog`` is fetched from Keystone.
""" """
def __init__(self, service_name): def __init__(self, service_name):
message = 'Invalid service catalog service: %s' % service_name message = 'Invalid service catalog service: %s' % service_name
@ -109,9 +113,7 @@ class ServiceCatalogException(HorizonException):
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class AlreadyExists(HorizonException): class AlreadyExists(HorizonException):
"""Exception to be raised when trying to create an API resource which """API resources tried to create already exists."""
already exists.
"""
def __init__(self, name, resource_type): def __init__(self, name, resource_type):
self.attrs = {"name": name, "resource": resource_type} self.attrs = {"name": name, "resource": resource_type}
self.msg = _('A %(resource)s with the name "%(name)s" already exists.') self.msg = _('A %(resource)s with the name "%(name)s" already exists.')
@ -125,8 +127,10 @@ class AlreadyExists(HorizonException):
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class GetFileError(HorizonException): class GetFileError(HorizonException):
"""Exception to be raised when the value of get_file did not start with """Exception to be raised when the value of get_file is not expected.
https:// or http://
The expected values start with https:// or http://.
Otherwise this exception will be raised.
""" """
def __init__(self, name, resource_type): def __init__(self, name, resource_type):
self.attrs = {"name": name, "resource": resource_type} self.attrs = {"name": name, "resource": resource_type}
@ -159,7 +163,9 @@ class WorkflowError(HorizonException):
class WorkflowValidationError(HorizonException): class WorkflowValidationError(HorizonException):
"""Exception raised during workflow validation if required data is missing, """Exception raised during workflow validation.
It is raised if required data is missing,
or existing data is not valid. or existing data is not valid.
""" """
pass pass
@ -171,7 +177,9 @@ class MessageFailure(HorizonException):
class HandledException(HorizonException): class HandledException(HorizonException):
"""Used internally to track exceptions that have gone through """Used internally to track exceptions that are already handled.
It is used to track exceptions that have gone through
:func:`horizon.exceptions.handle` more than once. :func:`horizon.exceptions.handle` more than once.
""" """
def __init__(self, wrapped): def __init__(self, wrapped):
@ -192,8 +200,10 @@ def error_color(msg):
def check_message(keywords, message): def check_message(keywords, message):
"""Checks an exception for given keywords and raises a new ``ActionError`` """Checks an exception for given keywords and raises an error if found.
with the desired message if the keywords are found. This allows selective
It raises a new ``ActionError`` with the desired message if the
keywords are found. This allows selective
control over API error messages. control over API error messages.
""" """
exc_type, exc_value, exc_traceback = sys.exc_info() exc_type, exc_value, exc_traceback = sys.exc_info()

View File

@ -30,16 +30,15 @@ class SelfHandlingMixin(object):
class SelfHandlingForm(SelfHandlingMixin, forms.Form): class SelfHandlingForm(SelfHandlingMixin, forms.Form):
"""A base :class:`Form <django:django.forms.Form>` class which includes """A base Form class which includes processing logic in its subclasses."""
processing logic in its subclasses.
"""
required_css_class = 'required' required_css_class = 'required'
def api_error(self, message): def api_error(self, message):
"""Adds an error to the form's error dictionary after validation """Adds an error to the form's error dictionary.
based on problems reported via the API. This is useful when you
wish for API errors to appear as errors on the form rather than It can be used after validation based on problems reported via the API.
using the messages framework. This is useful when you wish for API errors to appear as errors on the
form rather than using the messages framework.
""" """
self.add_error(NON_FIELD_ERRORS, message) self.add_error(NON_FIELD_ERRORS, message)

View File

@ -40,6 +40,7 @@ IPv6 = 2
class IPField(fields.Field): class IPField(fields.Field):
"""Form field for entering IP/range values, with validation. """Form field for entering IP/range values, with validation.
Supports IPv4/IPv6 in the format: Supports IPv4/IPv6 in the format:
.. xxx.xxx.xxx.xxx .. xxx.xxx.xxx.xxx
.. xxx.xxx.xxx.xxx/zz .. xxx.xxx.xxx.xxx/zz
@ -157,9 +158,10 @@ class MACAddressField(fields.Field):
class SelectWidget(widgets.Select): class SelectWidget(widgets.Select):
"""Customizable select widget, that allows to render """Customizable select widget.
data-xxx attributes from choices. This widget also
allows user to specify additional html attributes It allows to render data-xxx attributes from choices.
This widget also allows user to specify additional html attributes
for choices. for choices.
.. attribute:: data_attrs .. attribute:: data_attrs
@ -301,7 +303,9 @@ class ThemableSelectWidget(SelectWidget):
class DynamicSelectWidget(SelectWidget): class DynamicSelectWidget(SelectWidget):
"""A subclass of the ``Select`` widget which renders extra attributes for """``Select`` widget to handle dynamic changes to the available choices.
A subclass of the ``Select`` widget which renders extra attributes for
use in callbacks to handle dynamic changes to the available choices. use in callbacks to handle dynamic changes to the available choices.
""" """
_data_add_url_attr = "data-add-item-url" _data_add_url_attr = "data-add-item-url"
@ -335,8 +339,7 @@ class ThemableChoiceField(fields.ChoiceField):
class DynamicChoiceField(fields.ChoiceField): class DynamicChoiceField(fields.ChoiceField):
"""A subclass of ``ChoiceField`` with additional properties that make """ChoiceField that make dynamically updating its elements easier.
dynamically updating its elements easier.
Notably, the field declaration takes an extra argument, ``add_item_link`` Notably, the field declaration takes an extra argument, ``add_item_link``
which may be a string or callable defining the URL that should be used which may be a string or callable defining the URL that should be used
@ -370,8 +373,9 @@ class ThemableDynamicTypedChoiceField(ThemableDynamicChoiceField,
class ThemableCheckboxInput(widgets.CheckboxInput): class ThemableCheckboxInput(widgets.CheckboxInput):
"""A subclass of the ``Checkbox`` widget which renders extra markup to """Checkbox widget which renders extra markup.
allow a custom checkbox experience.
It is used to allow a custom checkbox experience.
""" """
def render(self, name, value, attrs=None): def render(self, name, value, attrs=None):
label_for = attrs.get('id', '') label_for = attrs.get('id', '')
@ -411,10 +415,12 @@ class ThemableCheckboxSelectMultiple(widgets.CheckboxSelectMultiple):
class ExternalFileField(fields.FileField): class ExternalFileField(fields.FileField):
"""A special flavor of FileField which is meant to be used in cases when """Special FileField to upload file to some external location.
instead of uploading file to Django it should be uploaded to some external
location, while the form validation is done as usual. Should be paired This is a special flavor of FileField which is meant to be used in cases
with ExternalUploadMeta metaclass embedded into the Form class. when instead of uploading file to Django it should be uploaded to some
external location, while the form validation is done as usual. It should be
paired with ExternalUploadMeta metaclass embedded into the Form class.
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ExternalFileField, self).__init__(*args, **kwargs) super(ExternalFileField, self).__init__(*args, **kwargs)
@ -422,9 +428,11 @@ class ExternalFileField(fields.FileField):
class ExternalUploadMeta(forms.DeclarativeFieldsMetaclass): class ExternalUploadMeta(forms.DeclarativeFieldsMetaclass):
"""Set this class as the metaclass of a form that contains """Metaclass to process ExternalFileField fields in a specific way.
ExternalFileField in order to process ExternalFileField fields in a
specific way. A hidden CharField twin of FieldField is created which Set this class as the metaclass of a form that contains ExternalFileField
in order to process ExternalFileField fields in a specific way.
A hidden CharField twin of FieldField is created which
contains just the filename (if any file was selected on browser side) and contains just the filename (if any file was selected on browser side) and
a special `clean` method for FileField is defined which extracts just file a special `clean` method for FileField is defined which extracts just file
name. This allows to avoid actual file upload to Django server, yet name. This allows to avoid actual file upload to Django server, yet

View File

@ -27,7 +27,9 @@ ADD_TO_FIELD_HEADER = "HTTP_X_HORIZON_ADD_TO_FIELD"
class ModalBackdropMixin(object): class ModalBackdropMixin(object):
"""This mixin class is to be used for together with ModalFormView and """Mixin class to allow ModalFormView and WorkflowView together.
This mixin class is to be used for together with ModalFormView and
WorkflowView classes to augment them with modal_backdrop context data. WorkflowView classes to augment them with modal_backdrop context data.
.. attribute: modal_backdrop (optional) .. attribute: modal_backdrop (optional)
@ -78,8 +80,10 @@ class ModalFormMixin(ModalBackdropMixin):
class ModalFormView(ModalFormMixin, views.HorizonFormView): class ModalFormView(ModalFormMixin, views.HorizonFormView):
"""The main view class from which all views which handle forms in Horizon """The main view class for all views which handle forms in Horizon.
should inherit. It takes care of all details with processing
All view which handles forms in Horiozn should inherit this class.
It takes care of all details with processing
:class:`~horizon.forms.base.SelfHandlingForm` classes, and modal concerns :class:`~horizon.forms.base.SelfHandlingForm` classes, and modal concerns
when the associated template inherits from when the associated template inherits from
`horizon/common/_modal_form.html`. `horizon/common/_modal_form.html`.
@ -148,16 +152,20 @@ class ModalFormView(ModalFormMixin, views.HorizonFormView):
return self.cancel_url or self.success_url return self.cancel_url or self.success_url
def get_object_id(self, obj): def get_object_id(self, obj):
"""For dynamic insertion of resources created in modals, this method """Returns the ID of the created object.
returns the id of the created object. Defaults to returning the ``id``
attribute. For dynamic insertion of resources created in modals,
this method returns the id of the created object.
Defaults to returning the ``id`` attribute.
""" """
return obj.id return obj.id
def get_object_display(self, obj): def get_object_display(self, obj):
"""For dynamic insertion of resources created in modals, this method """Returns the display name of the created object.
returns the display name of the created object. Defaults to returning
the ``name`` attribute. For dynamic insertion of resources created in modals,
this method returns the display name of the created object.
Defaults to returning the ``name`` attribute.
""" """
return obj.name return obj.name

View File

@ -111,8 +111,10 @@ class HorizonMiddleware(object):
timezone.activate(tz) timezone.activate(tz)
def process_exception(self, request, exception): def process_exception(self, request, exception):
"""Catches internal Horizon exception classes such as NotAuthorized, """Catches internal Horizon exception classes.
NotFound and Http302 and handles them gracefully.
Exception classes such as NotAuthorized, NotFound and Http302
are caught and handles them gracefully.
""" """
if isinstance(exception, (exceptions.NotAuthorized, if isinstance(exception, (exceptions.NotAuthorized,
exceptions.NotAuthenticated)): exceptions.NotAuthenticated)):
@ -155,8 +157,9 @@ class HorizonMiddleware(object):
dst[header] = src[header] dst[header] = src[header]
def process_response(self, request, response): def process_response(self, request, response):
"""Convert HttpResponseRedirect to HttpResponse if request is via ajax """Convert HttpResponseRedirect to HttpResponse if request is via ajax.
to allow ajax request to redirect url
This is to allow ajax request to redirect url.
""" """
if request.is_ajax() and hasattr(request, 'horizon'): if request.is_ajax() and hasattr(request, 'horizon'):
queued_msgs = request.horizon['async_messages'] queued_msgs = request.horizon['async_messages']

View File

@ -42,8 +42,8 @@ STRING_SEPARATOR = "__"
class BaseActionMetaClass(type): class BaseActionMetaClass(type):
"""Metaclass for adding all actions options from inheritance tree """Metaclass for adding all actions options from inheritance tree to action.
to action.
This way actions can inherit from each other but still use This way actions can inherit from each other but still use
the class attributes DSL. Meaning, all attributes of Actions are the class attributes DSL. Meaning, all attributes of Actions are
defined as class attributes, but in the background, it will be used as defined as class attributes, but in the background, it will be used as
@ -99,6 +99,7 @@ class BaseAction(html.HTMLElement):
def data_type_matched(self, datum): def data_type_matched(self, datum):
"""Method to see if the action is allowed for a certain type of data. """Method to see if the action is allowed for a certain type of data.
Only affects mixed data type tables. Only affects mixed data type tables.
""" """
if datum: if datum:
@ -148,13 +149,15 @@ class BaseAction(html.HTMLElement):
pass pass
def get_default_classes(self): def get_default_classes(self):
"""Returns a list of the default classes for the action. Defaults to """Returns a list of the default classes for the action.
``["btn", "btn-default", "btn-sm"]``.
Defaults to ``["btn", "btn-default", "btn-sm"]``.
""" """
return getattr(settings, "ACTION_CSS_CLASSES", ACTION_CSS_CLASSES) return getattr(settings, "ACTION_CSS_CLASSES", ACTION_CSS_CLASSES)
def get_default_attrs(self): def get_default_attrs(self):
"""Returns a list of the default HTML attributes for the action. """Returns a list of the default HTML attributes for the action.
Defaults to returning an ``id`` attribute with the value Defaults to returning an ``id`` attribute with the value
``{{ table.name }}__action_{{ action.name }}__{{ creation counter }}``. ``{{ table.name }}__action_{{ action.name }}__{{ creation counter }}``.
""" """
@ -527,9 +530,7 @@ class FilterAction(BaseAction):
return data return data
def is_api_filter(self, filter_field): def is_api_filter(self, filter_field):
"""Determine if the given filter field should be used as an """Determine if agiven filter field should be used as an API filter."""
API filter.
"""
if self.filter_type == 'server': if self.filter_type == 'server':
for choice in self.filter_choices: for choice in self.filter_choices:
if (choice[0] == filter_field and len(choice) > 2 and if (choice[0] == filter_field and len(choice) > 2 and
@ -538,8 +539,9 @@ class FilterAction(BaseAction):
return False return False
def get_select_options(self): def get_select_options(self):
"""Provide the value, string, and help_text (if applicable) """Provide the value, string, and help_text for the template to render.
for the template to render.
help_text is returned if applicable.
""" """
if self.filter_choices: if self.filter_choices:
return [choice[:4] for choice in self.filter_choices return [choice[:4] for choice in self.filter_choices
@ -579,8 +581,7 @@ class FixedFilterAction(FilterAction):
return self.categories[filter_string] return self.categories[filter_string]
def get_fixed_buttons(self): def get_fixed_buttons(self):
"""Returns a list of dictionaries describing the fixed buttons """Returns a list of dict describing fixed buttons used for filtering.
to use for filtering.
Each list item should be a dict with the following keys: Each list item should be a dict with the following keys:
@ -605,9 +606,9 @@ class FixedFilterAction(FilterAction):
class BatchAction(Action): class BatchAction(Action):
"""A table action which takes batch action on one or more """A table action which takes batch action on one or more objects.
objects. This action should not require user input on a
per-object basis. This action should not require user input on a per-object basis.
.. attribute:: name .. attribute:: name
@ -723,8 +724,9 @@ class BatchAction(Action):
return action return action
def action(self, request, datum_id): def action(self, request, datum_id):
"""Required. Accepts a single object id and performs the specific """Accepts a single object id and performs the specific action.
action.
This method is required.
Return values are discarded, errors raised are caught and logged. Return values are discarded, errors raised are caught and logged.
""" """

View File

@ -363,8 +363,9 @@ class Column(html.HTMLElement):
return '<%s: %s>' % (self.__class__.__name__, self.name) return '<%s: %s>' % (self.__class__.__name__, self.name)
def allowed(self, request): def allowed(self, request):
"""Determine whether processing/displaying the column is allowed """Determine whether processing/displaying the column is allowed.
for the current request.
It is determined based on the current request.
""" """
if not self.policy_rules: if not self.policy_rules:
return True return True
@ -376,9 +377,10 @@ class Column(html.HTMLElement):
return True return True
def get_raw_data(self, datum): def get_raw_data(self, datum):
"""Returns the raw data for this column, before any filters or """Returns the raw data for this column.
formatting are applied to it. This is useful when doing calculations
on data in the table. No filters or formatting are applied to the returned data.
This is useful when doing calculations on data in the table.
""" """
# Callable transformations # Callable transformations
if callable(self.transform): if callable(self.transform):
@ -397,8 +399,7 @@ class Column(html.HTMLElement):
return data return data
def get_data(self, datum): def get_data(self, datum):
"""Returns the final display data for this column from the given """Returns the final display data for this column from the given inputs.
inputs.
The return value will be either the attribute specified for this column The return value will be either the attribute specified for this column
or the return value of the attr:`~horizon.tables.Column.transform` or the return value of the attr:`~horizon.tables.Column.transform`
@ -471,8 +472,10 @@ class Column(html.HTMLElement):
return attrs return attrs
def get_summation(self): def get_summation(self):
"""Returns the summary value for the data in this column if a """Returns the summary value for the data in this column.
valid summation method is specified for it. Otherwise returns ``None``.
It returns the summary value if a valid summation method is
specified for it. Otherwise returns ``None``.
""" """
if self.summation not in self.summation_methods: if self.summation not in self.summation_methods:
return None return None
@ -577,11 +580,14 @@ class Row(html.HTMLElement):
self.cells = [] self.cells = []
def load_cells(self, datum=None): def load_cells(self, datum=None):
"""Load the row's data (either provided at initialization or as an """Load the row's data and initialize all the cells in the row.
argument to this function), initialize all the cells contained
by this row, and set the appropriate row properties which require It also set the appropriate row properties which require
the row's data to be determined. the row's data to be determined.
The row's data is provided either at initialization or as an
argument to this function.
This function is called automatically by This function is called automatically by
:meth:`~horizon.tables.Row.__init__` if the ``datum`` argument is :meth:`~horizon.tables.Row.__init__` if the ``datum`` argument is
provided. However, by not providing the data during initialization provided. However, by not providing the data during initialization
@ -665,14 +671,17 @@ class Row(html.HTMLElement):
return "%s?%s" % (table_url, params) return "%s?%s" % (table_url, params)
def can_be_selected(self, datum): def can_be_selected(self, datum):
"""By default if multiselect enabled return True. You can remove the """Determines whether the row can be selected.
checkbox after an ajax update here if required.
By default if multiselect enabled return True.
You can remove the checkbox after an ajax update here if required.
""" """
return True return True
def get_data(self, request, obj_id): def get_data(self, request, obj_id):
"""Fetches the updated data for the row based on the object id """Fetches the updated data for the row based on the given object ID.
passed in. Must be implemented by a subclass to allow AJAX updating.
Must be implemented by a subclass to allow AJAX updating.
""" """
return {} return {}
@ -1322,9 +1331,11 @@ class DataTable(object):
return str(slugify(self._meta.name)) return str(slugify(self._meta.name))
def get_filter_string(self): def get_filter_string(self):
"""Get the filter string value. For 'server' type filters this is """Get the filter string value.
saved in the session so that it gets persisted across table loads.
For other filter types this is obtained from the POST dict. For 'server' type filters this is saved in the session so that
it gets persisted across table loads. For other filter types
this is obtained from the POST dict.
""" """
filter_action = self._meta._filter_action filter_action = self._meta._filter_action
param_name = filter_action.get_param_name() param_name = filter_action.get_param_name()
@ -1336,8 +1347,9 @@ class DataTable(object):
return filter_string return filter_string
def get_filter_field(self): def get_filter_field(self):
"""Get the filter field value used for 'server' type filters. This """Get the filter field value used for 'server' type filters.
is the value from the filter action's list of filter choices.
This is the value from the filter action's list of filter choices.
""" """
filter_action = self._meta._filter_action filter_action = self._meta._filter_action
param_name = '%s_field' % filter_action.get_param_name() param_name = '%s_field' % filter_action.get_param_name()
@ -1406,15 +1418,19 @@ class DataTable(object):
return self._no_data_message return self._no_data_message
def get_filter_first_message(self): def get_filter_first_message(self):
"""Return the message to be displayed when the user needs to provide """Return the message to be displayed first in the filter.
first a search criteria before loading any data.
when the user needs to provide a search criteria first
before loading any data.
""" """
return self._filter_first_message return self._filter_first_message
def get_object_by_id(self, lookup): def get_object_by_id(self, lookup):
"""Returns the data object from the table's dataset which matches """Returns the data object whose ID matches ``loopup`` parameter.
the ``lookup`` parameter specified. An error will be raised if
the match is not a single data object. The data object is looked up from the table's dataset and
the data which matches the ``lookup`` parameter specified.
An error will be raised if the match is not a single data object.
We will convert the object id and ``lookup`` to unicode before We will convert the object id and ``lookup`` to unicode before
comparison. comparison.
@ -1445,8 +1461,9 @@ class DataTable(object):
@property @property
def has_actions(self): def has_actions(self):
"""Boolean. Indicates whether there are any available actions on this """Indicates whether there are any available actions on this table.
table.
Returns a boolean value.
""" """
if not self.base_actions: if not self.base_actions:
return False return False
@ -1454,8 +1471,9 @@ class DataTable(object):
@property @property
def needs_form_wrapper(self): def needs_form_wrapper(self):
"""Boolean. Indicates whether this table should be rendered wrapped in """Returns if this table should be rendered wrapped in a ``<form>`` tag.
a ``<form>`` tag or not.
Returns a boolean value.
""" """
# If needs_form_wrapper is explicitly set, defer to that. # If needs_form_wrapper is explicitly set, defer to that.
if self._needs_form_wrapper is not None: if self._needs_form_wrapper is not None:
@ -1532,8 +1550,10 @@ class DataTable(object):
return table_actions_template.render(context) return table_actions_template.render(context)
def render_row_actions(self, datum, row=False): def render_row_actions(self, datum, row=False):
"""Renders the actions specified in ``Meta.row_actions`` using the """Renders the actions specified in ``Meta.row_actions``.
current row data. If `row` is True, the actions are rendered in a row
The actions are rendered using the current row data.
If `row` is True, the actions are rendered in a row
of buttons. Otherwise they are rendered in a dropdown box. of buttons. Otherwise they are rendered in a dropdown box.
""" """
if row: if row:
@ -1550,8 +1570,9 @@ class DataTable(object):
@staticmethod @staticmethod
def parse_action(action_string): def parse_action(action_string):
"""Parses the ``action`` parameter (a string) sent back with the """Parses the ``action_string`` parameter sent back with the POST data.
POST data. By default this parses a string formatted as
By default this parses a string formatted as
``{{ table_name }}__{{ action_name }}__{{ row_id }}`` and returns ``{{ table_name }}__{{ action_name }}__{{ row_id }}`` and returns
each of the pieces. The ``row_id`` is optional. each of the pieces. The ``row_id`` is optional.
""" """
@ -1568,10 +1589,10 @@ class DataTable(object):
return table, action, object_id return table, action, object_id
def take_action(self, action_name, obj_id=None, obj_ids=None): def take_action(self, action_name, obj_id=None, obj_ids=None):
"""Locates the appropriate action and routes the object """Locates the appropriate action and routes the object data to it.
data to it. The action should return an HTTP redirect
if successful, or a value which evaluates to ``False`` The action should return an HTTP redirect if successful,
if unsuccessful. or a value which evaluates to ``False`` if unsuccessful.
""" """
# See if we have a list of ids # See if we have a list of ids
obj_ids = obj_ids or self.request.POST.getlist('object_ids') obj_ids = obj_ids or self.request.POST.getlist('object_ids')
@ -1616,8 +1637,10 @@ class DataTable(object):
return table, action, obj_id return table, action, obj_id
def maybe_preempt(self): def maybe_preempt(self):
"""Determine whether the request should be handled by a preemptive """Determine whether the request should be handled in earlier phase.
action on this table or by an AJAX row update before loading any data.
It determines the request should be handled by a preemptive action
on this table or by an AJAX row update before loading any data.
""" """
request = self.request request = self.request
table_name, action_name, obj_id = self.check_handler(request) table_name, action_name, obj_id = self.check_handler(request)
@ -1705,8 +1728,7 @@ class DataTable(object):
return HttpResponse(status=error.status_code) return HttpResponse(status=error.status_code)
def inline_update_action(self, request, datum, cell, obj_id, cell_name): def inline_update_action(self, request, datum, cell, obj_id, cell_name):
"""Handling update by POST of the cell. """Handling update by POST of the cell."""
"""
new_cell_value = request.POST.get( new_cell_value = request.POST.get(
cell_name + '__' + obj_id, None) cell_name + '__' + obj_id, None)
if issubclass(cell.column.form_field.__class__, if issubclass(cell.column.form_field.__class__,
@ -1742,7 +1764,9 @@ class DataTable(object):
content_type="application/json") content_type="application/json")
def maybe_handle(self): def maybe_handle(self):
"""Determine whether the request should be handled by any action on """Handles table actions if needed.
It determines whether the request should be handled by any action on
this table after data has been loaded. this table after data has been loaded.
""" """
request = self.request request = self.request
@ -1756,8 +1780,10 @@ class DataTable(object):
return None return None
def sanitize_id(self, obj_id): def sanitize_id(self, obj_id):
"""Override to modify an incoming obj_id to match existing """Override to modify an incoming obj_id to match existing API.
API data types or modify the format.
It is used to modify an incoming obj_id (used in Horizon)
to the data type or format expected by the API.
""" """
return obj_id return obj_id
@ -1787,8 +1813,10 @@ class DataTable(object):
return getattr(datum, display_key, None) return getattr(datum, display_key, None)
def has_prev_data(self): def has_prev_data(self):
"""Returns a boolean value indicating whether there is previous data """Returns a boolean value indicating whether there is previous data.
available to this table from the source (generally an API).
Returns True if there is previous data available to this table
from the source (generally an API).
The method is largely meant for internal use, but if you want to The method is largely meant for internal use, but if you want to
override it to provide custom behavior you can do so at your own risk. override it to provide custom behavior you can do so at your own risk.
@ -1796,8 +1824,10 @@ class DataTable(object):
return self._meta.has_prev_data return self._meta.has_prev_data
def has_more_data(self): def has_more_data(self):
"""Returns a boolean value indicating whether there is more data """Returns a boolean value indicating whether there is more data.
available to this table from the source (generally an API).
Returns True if there is more data available to this table
from the source (generally an API).
The method is largely meant for internal use, but if you want to The method is largely meant for internal use, but if you want to
override it to provide custom behavior you can do so at your own risk. override it to provide custom behavior you can do so at your own risk.
@ -1805,35 +1835,35 @@ class DataTable(object):
return self._meta.has_more_data return self._meta.has_more_data
def get_prev_marker(self): def get_prev_marker(self):
"""Returns the identifier for the first object in the current data set """Returns the identifier for the first object in the current data set.
for APIs that use marker/limit-based paging.
The return value will be used as marker/limit-based paging in the API.
""" """
return http.urlquote_plus(self.get_object_id(self.data[0])) \ return http.urlquote_plus(self.get_object_id(self.data[0])) \
if self.data else '' if self.data else ''
def get_marker(self): def get_marker(self):
"""Returns the identifier for the last object in the current data set """Returns the identifier for the last object in the current data set.
for APIs that use marker/limit-based paging.
The return value will be used as marker/limit-based paging in the API.
""" """
return http.urlquote_plus(self.get_object_id(self.data[-1])) \ return http.urlquote_plus(self.get_object_id(self.data[-1])) \
if self.data else '' if self.data else ''
def get_prev_pagination_string(self): def get_prev_pagination_string(self):
"""Returns the query parameter string to paginate this table """Returns the query parameter string to paginate to the prev page."""
to the previous page.
"""
return "=".join([self._meta.prev_pagination_param, return "=".join([self._meta.prev_pagination_param,
self.get_prev_marker()]) self.get_prev_marker()])
def get_pagination_string(self): def get_pagination_string(self):
"""Returns the query parameter string to paginate this table """Returns the query parameter string to paginate to the next page."""
to the next page.
"""
return "=".join([self._meta.pagination_param, self.get_marker()]) return "=".join([self._meta.pagination_param, self.get_marker()])
def calculate_row_status(self, statuses): def calculate_row_status(self, statuses):
"""Returns a boolean value determining the overall row status """Returns a boolean value determining the overall row status.
based on the dictionary of column name to status mappings passed in.
It is detremined based on the dictionary of column name
to status mappings passed in.
By default, it uses the following logic: By default, it uses the following logic:
@ -1858,9 +1888,10 @@ class DataTable(object):
return True return True
def get_row_status_class(self, status): def get_row_status_class(self, status):
"""Returns a css class name determined by the status value. This class """Returns a css class name determined by the status value.
name is used to indicate the status of the rows in the table if
any ``status_columns`` have been specified. This class name is used to indicate the status of the rows in the table
if any ``status_columns`` have been specified.
""" """
if status is True: if status is True:
return "status_up" return "status_up"

View File

@ -158,8 +158,9 @@ class MultiTableMixin(object):
return filter_info return filter_info
def handle_server_filter(self, request, table=None): def handle_server_filter(self, request, table=None):
"""Update the table server filter information in the session and """Update the table server filter information in the session.
determine if the filter has been changed.
Returns True if the filter has been changed.
""" """
if not table: if not table:
table = self.get_table() table = self.get_table()
@ -172,9 +173,10 @@ class MultiTableMixin(object):
return filter_info['changed'] return filter_info['changed']
def update_server_filter_action(self, request, table=None): def update_server_filter_action(self, request, table=None):
"""Update the table server side filter action based on the current """Update the table server side filter action.
filter. The filter info may be stored in the session and this will
restore it. It is done based on the current filter. The filter info may be stored
in the session and this will restore it.
""" """
if not table: if not table:
table = self.get_table() table = self.get_table()
@ -187,8 +189,10 @@ class MultiTableMixin(object):
class MultiTableView(MultiTableMixin, views.HorizonTemplateView): class MultiTableView(MultiTableMixin, views.HorizonTemplateView):
"""A class-based generic view to handle the display and processing of """Generic view to handle multiple DataTable classes in a single view.
multiple :class:`~horizon.tables.DataTable` classes in a single view.
Each DataTable class must be a :class:`~horizon.tables.DataTable` class
or its subclass.
Three steps are required to use this view: set the ``table_classes`` Three steps are required to use this view: set the ``table_classes``
attribute with a tuple of the desired attribute with a tuple of the desired
@ -309,8 +313,7 @@ class DataTableView(MultiTableView):
class MixedDataTableView(DataTableView): class MixedDataTableView(DataTableView):
"""A class-based generic view to handle DataTable with mixed data """A class-based generic view to handle DataTable with mixed data types.
types.
Basic usage is the same as DataTableView. Basic usage is the same as DataTableView.

View File

@ -30,8 +30,7 @@ CSS_DISABLED_TAB_CLASSES = ["disabled"]
class TabGroup(html.HTMLElement): class TabGroup(html.HTMLElement):
"""A container class which knows how to manage and render """A container class which knows how to manage and render Tab objects.
:class:`~horizon.tabs.Tab` objects.
.. attribute:: slug .. attribute:: slug
@ -128,21 +127,26 @@ class TabGroup(html.HTMLElement):
exceptions.handle(self.request) exceptions.handle(self.request)
def get_id(self): def get_id(self):
"""Returns the id for this tab group. Defaults to the value of the tab """Returns the id for this tab group.
Defaults to the value of the tab
group's :attr:`horizon.tabs.Tab.slug`. group's :attr:`horizon.tabs.Tab.slug`.
""" """
return self.slug return self.slug
def get_default_classes(self): def get_default_classes(self):
"""Returns a list of the default classes for the tab group. Defaults to """Returns a list of the default classes for the tab group.
``["nav", "nav-tabs", "ajax-tabs"]``.
Defaults to ``["nav", "nav-tabs", "ajax-tabs"]``.
""" """
default_classes = super(TabGroup, self).get_default_classes() default_classes = super(TabGroup, self).get_default_classes()
default_classes.extend(CSS_TAB_GROUP_CLASSES) default_classes.extend(CSS_TAB_GROUP_CLASSES)
return default_classes return default_classes
def tabs_not_available(self): def tabs_not_available(self):
"""In the event that no tabs are either allowed or enabled, this method """The fallback handler if no tabs are either allowed or enabled.
In the event that no tabs are either allowed or enabled, this method
is the fallback handler. By default it's a no-op, but it exists is the fallback handler. By default it's a no-op, but it exists
to make redirecting or raising exceptions possible for subclasses. to make redirecting or raising exceptions possible for subclasses.
""" """
@ -214,8 +218,7 @@ class TabGroup(html.HTMLElement):
class Tab(html.HTMLElement): class Tab(html.HTMLElement):
"""A reusable interface for constructing a tab within a """A reusable interface for constructing a tab within a TabGroup.
:class:`~horizon.tabs.TabGroup`.
.. attribute:: name .. attribute:: name
@ -301,9 +304,10 @@ class Tab(html.HTMLElement):
return getattr(self, "_data", None) is not None return getattr(self, "_data", None) is not None
def render(self): def render(self):
"""Renders the tab to HTML using the """Renders the tab to HTML.
:meth:`~horizon.tabs.Tab.get_context_data` method and :meth:`~horizon.tabs.Tab.get_context_data` method and
the :meth:`~horizon.tabs.Tab.get_template_name` method. the :meth:`~horizon.tabs.Tab.get_template_name` method are called.
If :attr:`~horizon.tabs.Tab.preload` is ``False`` and ``force_load`` If :attr:`~horizon.tabs.Tab.preload` is ``False`` and ``force_load``
is not ``True``, or is not ``True``, or
@ -323,8 +327,9 @@ class Tab(html.HTMLElement):
return render_to_string(self.get_template_name(self.request), context) return render_to_string(self.get_template_name(self.request), context)
def get_id(self): def get_id(self):
"""Returns the id for this tab. Defaults to """Returns the id for this tab.
``"{{ tab_group.slug }}__{{ tab.slug }}"``.
Defaults to ``"{{ tab_group.slug }}__{{ tab.slug }}"``.
""" """
return SEPARATOR.join([self.tab_group.slug, self.slug]) return SEPARATOR.join([self.tab_group.slug, self.slug])
@ -332,9 +337,10 @@ class Tab(html.HTMLElement):
return "=".join((self.tab_group.param_name, self.get_id())) return "=".join((self.tab_group.param_name, self.get_id()))
def get_default_classes(self): def get_default_classes(self):
"""Returns a list of the default classes for the tab. Defaults to """Returns a list of the default classes for the tab.
and empty list (``[]``), however additional classes may be added
depending on the state of the tab as follows: Defaults to and empty list (``[]``), however additional classes may
be added depending on the state of the tab as follows:
If the tab is the active tab for the tab group, in which If the tab is the active tab for the tab group, in which
the class ``"active"`` will be added. the class ``"active"`` will be added.
@ -362,14 +368,17 @@ class Tab(html.HTMLElement):
return self.template_name return self.template_name
def get_context_data(self, request, **kwargs): def get_context_data(self, request, **kwargs):
"""This method should return a dictionary of context data used to """Return a dictionary of context data used to render the tab.
render the tab. Required.
Required.
""" """
return kwargs return kwargs
def enabled(self, request): def enabled(self, request):
"""Determines whether or not the tab should be accessible """Determines whether or not the tab should be accessible.
(e.g. be rendered into the HTML on load and respond to a click event).
For example, the tab should be rendered into the HTML
on load and respond to a click event.
If a tab returns ``False`` from ``enabled`` it will ignore the value If a tab returns ``False`` from ``enabled`` it will ignore the value
of ``preload`` and only render the HTML of the tab after being clicked. of ``preload`` and only render the HTML of the tab after being clicked.
@ -400,8 +409,7 @@ class Tab(html.HTMLElement):
class TableTab(Tab): class TableTab(Tab):
"""A :class:`~horizon.tabs.Tab` class which knows how to deal with """A Tab class which knows how to deal with DataTable classes inside of it.
:class:`~horizon.tables.DataTable` classes rendered inside of it.
This distinct class is required due to the complexity involved in handling This distinct class is required due to the complexity involved in handling
both dynamic tab loading, dynamic table updating and table actions all both dynamic tab loading, dynamic table updating and table actions all
@ -432,8 +440,9 @@ class TableTab(Tab):
self._table_data_loaded = False self._table_data_loaded = False
def load_table_data(self): def load_table_data(self):
"""Calls the ``get_{{ table_name }}_data`` methods for each table class """Calls the ``get_{{ table_name }}_data`` methods for each table class.
and sets the data on the tables.
When returning, the loaded data is set on the tables.
""" """
# We only want the data to be loaded once, so we track if we have... # We only want the data to be loaded once, so we track if we have...
if not self._table_data_loaded: if not self._table_data_loaded:
@ -456,8 +465,10 @@ class TableTab(Tab):
self._table_data_loaded = True self._table_data_loaded = True
def get_context_data(self, request, **kwargs): def get_context_data(self, request, **kwargs):
"""Adds a ``{{ table_name }}_table`` item to the context for each table """Adds a ``{{ table_name }}_table`` item to the context for each table.
in the :attr:`~horizon.tabs.TableTab.table_classes` attribute.
The target tables are specified by
the :attr:`~horizon.tabs.TableTab.table_classes` attribute.
If only one table class is provided, a shortcut ``table`` context If only one table class is provided, a shortcut ``table`` context
variable is also added containing the single table. variable is also added containing the single table.

View File

@ -19,8 +19,7 @@ from horizon import views
class TabView(views.HorizonTemplateView): class TabView(views.HorizonTemplateView):
"""A generic class-based view for displaying a """A generic view for displaying a :class:`horizon.tabs.TabGroup`.
:class:`horizon.tabs.TabGroup`.
This view handles selecting specific tabs and deals with AJAX requests This view handles selecting specific tabs and deals with AJAX requests
gracefully. gracefully.
@ -57,8 +56,9 @@ class TabView(views.HorizonTemplateView):
return context return context
def handle_tabbed_response(self, tab_group, context): def handle_tabbed_response(self, tab_group, context):
"""Sends back an AJAX-appropriate response for the tab group if """Sends back an AJAX-appropriate response for the tab group if needed.
required, otherwise renders the response as normal.
Otherwise renders the response as normal.
""" """
if self.request.is_ajax(): if self.request.is_ajax():
if tab_group.selected: if tab_group.selected:
@ -79,10 +79,11 @@ class TabbedTableView(tables.MultiTableMixin, TabView):
self._table_dict = {} self._table_dict = {}
def load_tabs(self): def load_tabs(self):
"""Loads the tab group, and compiles the table instances for each """Loads the tab group.
table attached to any :class:`horizon.tabs.TableTab` instances on
the tab group. This step is necessary before processing any It compiles the table instances for each table attached to
tab or table actions. any :class:`horizon.tabs.TableTab` instances on the tab group.
This step is necessary before processing any tab or table actions.
""" """
tab_group = self.get_tabs(self.request, **self.kwargs) tab_group = self.get_tabs(self.request, **self.kwargs)
tabs = tab_group.get_tabs() tabs = tab_group.get_tabs()
@ -99,10 +100,12 @@ class TabbedTableView(tables.MultiTableMixin, TabView):
return {} return {}
def handle_table(self, table_dict): def handle_table(self, table_dict):
"""For the given dict containing a ``DataTable`` and a ``TableTab`` """Loads the table data based on a given table_dict and handles them.
For the given dict containing a ``DataTable`` and a ``TableTab``
instance, it loads the table data for that tab and calls the instance, it loads the table data for that tab and calls the
table's :meth:`~horizon.tables.DataTable.maybe_handle` method. The table's :meth:`~horizon.tables.DataTable.maybe_handle` method.
return value will be the result of ``maybe_handle``. The return value will be the result of ``maybe_handle``.
""" """
table = table_dict['table'] table = table_dict['table']
tab = table_dict['tab'] tab = table_dict['tab']

View File

@ -25,12 +25,13 @@ register = template.Library()
@receiver(post_compress) @receiver(post_compress)
def update_angular_template_hash(sender, **kwargs): def update_angular_template_hash(sender, **kwargs):
"""Listen for compress events. If the angular templates """Listen for compress events.
have been re-compressed, also clear them from the Django
cache backend. This is important to allow deployers to If the angular templates have been re-compressed, also clear them
change a template file, re-compress, and not accidentally from the Django cache backend. This is important to allow
serve the old Django cached version of that content to deployers to change a template file, re-compress, and not
clients. accidentally serve the old Django cached version of that content
to clients.
""" """
context = kwargs['context'] # context the compressor is working with context = kwargs['context'] # context the compressor is working with
compressed = context['compressed'] # the compressed content compressed = context['compressed'] # the compressed content
@ -55,9 +56,11 @@ def update_angular_template_hash(sender, **kwargs):
@register.filter(name='angular_escapes') @register.filter(name='angular_escapes')
def angular_escapes(value): def angular_escapes(value):
"""Djangos 'escapejs' is too aggressive and inserts unicode. Provide """Provide a basic filter to allow angular template content for Angular.
a basic filter to allow angular template content to be used within
javascript strings. Djangos 'escapejs' is too aggressive and inserts unicode.
It provide a basic filter to allow angular template content to be used
within javascript strings.
Args: Args:
value: a string value: a string
@ -75,10 +78,11 @@ def angular_escapes(value):
@register.inclusion_tag('angular/angular_templates.html', takes_context=True) @register.inclusion_tag('angular/angular_templates.html', takes_context=True)
def angular_templates(context): def angular_templates(context):
"""For all static HTML templates, generate a dictionary of template """Generate a dictionary of template contents for all static HTML templates.
contents. If the template has been overridden by a theme, load the
override contents instead of the original HTML file. One use for If the template has been overridden by a theme, load the
this is to pre-populate the angular template cache. override contents instead of the original HTML file.
One use for this is to pre-populate the angular template cache.
Args: Args:
context: the context of the current Django template context: the context of the current Django template

View File

@ -24,6 +24,7 @@ register = template.Library()
@register.inclusion_tag('bootstrap/progress_bar.html') @register.inclusion_tag('bootstrap/progress_bar.html')
def bs_progress_bar(*args, **kwargs): def bs_progress_bar(*args, **kwargs):
"""A Standard Bootstrap Progress Bar. """A Standard Bootstrap Progress Bar.
http://getbootstrap.com/components/#progress http://getbootstrap.com/components/#progress
param args (Array of Numbers: 0-100): Percent of Progress Bars param args (Array of Numbers: 0-100): Percent of Progress Bars

View File

@ -21,6 +21,7 @@ register = template.Library()
takes_context=True) takes_context=True)
def breadcrumb_nav(context): def breadcrumb_nav(context):
"""A logic heavy function for automagically creating a breadcrumb. """A logic heavy function for automagically creating a breadcrumb.
It uses the dashboard, panel group(if it exists), then current panel. It uses the dashboard, panel group(if it exists), then current panel.
Can also use a "custom_breadcrumb" context item to add extra items. Can also use a "custom_breadcrumb" context item to add extra items.
""" """

View File

@ -43,9 +43,7 @@ class MinifiedNode(Node):
@register.filter @register.filter
def has_permissions(user, component): def has_permissions(user, component):
"""Checks if the given user meets the permissions requirements for """Checks if the given user meets the permissions requirements."""
the component.
"""
return user.has_perms(getattr(component, 'permissions', set())) return user.has_perms(getattr(component, 'permissions', set()))
@ -188,7 +186,9 @@ class JSTemplateNode(template.Node):
@register.tag @register.tag
def jstemplate(parser, token): def jstemplate(parser, token):
"""Replaces ``[[[`` and ``]]]`` with ``{{{`` and ``}}}``, """Templatetag to handle any of the Mustache-based templates.
Replaces ``[[[`` and ``]]]`` with ``{{{`` and ``}}}``,
``[[`` and ``]]`` with ``{{`` and ``}}`` and ``[[`` and ``]]`` with ``{{`` and ``}}`` and
``[%`` and ``%]`` with ``{%`` and ``%}`` to avoid conflicts ``[%`` and ``%]`` with ``{%`` and ``%}`` to avoid conflicts
with Django's template engine when using any of the Mustache-based with Django's template engine when using any of the Mustache-based

View File

@ -31,9 +31,7 @@ register = template.Library()
class ParseDateNode(template.Node): class ParseDateNode(template.Node):
def render(self, datestring): def render(self, datestring):
"""Parses a date-like input string into a timezone aware Python """Parses a date-like string into a timezone aware Python datetime."""
datetime.
"""
formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f", formats = ["%Y-%m-%dT%H:%M:%S.%f", "%Y-%m-%d %H:%M:%S.%f",
"%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"] "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S"]
if datestring: if datestring:

View File

@ -115,8 +115,7 @@ class RequestFactoryWithMessages(RequestFactory):
@unittest.skipIf(os.environ.get('SKIP_UNITTESTS', False), @unittest.skipIf(os.environ.get('SKIP_UNITTESTS', False),
"The SKIP_UNITTESTS env variable is set.") "The SKIP_UNITTESTS env variable is set.")
class TestCase(django_test.TestCase): class TestCase(django_test.TestCase):
"""Specialized base test case class for Horizon which gives access to """Base test case class for Horizon with numerous additional features.
numerous additional features:
* The ``mox`` mocking framework via ``self.mox``. * The ``mox`` mocking framework via ``self.mox``.
* A ``RequestFactory`` class which supports Django's ``contrib.messages`` * A ``RequestFactory`` class which supports Django's ``contrib.messages``
@ -176,15 +175,17 @@ class TestCase(django_test.TestCase):
self.assertNotRegex(text, unexpected_regexp, msg) self.assertNotRegex(text, unexpected_regexp, msg)
def assertNoMessages(self, response=None): def assertNoMessages(self, response=None):
"""Asserts that no messages have been attached by the """Asserts no messages have been attached by the messages framework.
``contrib.messages`` framework.
The expected messages framework is ``django.contrib.messages``.
""" """
self.assertMessageCount(response, success=0, warn=0, info=0, error=0) self.assertMessageCount(response, success=0, warn=0, info=0, error=0)
def assertMessageCount(self, response=None, **kwargs): def assertMessageCount(self, response=None, **kwargs):
"""Asserts that the specified number of messages have been attached """Asserts that the expected number of messages have been attached.
for various message types. Usage would look like
``self.assertMessageCount(success=1)``. The expected number of messages can be specified per message type.
Usage would look like ``self.assertMessageCount(success=1)``.
""" """
temp_req = self.client.request(**{'wsgi.input': None}) temp_req = self.client.request(**{'wsgi.input': None})
temp_req.COOKIES = self.client.cookies temp_req.COOKIES = self.client.cookies
@ -255,9 +256,9 @@ class SeleniumTestCase(LiveServerTestCase):
class JasmineTests(SeleniumTestCase): class JasmineTests(SeleniumTestCase):
"""Helper class which allows you to create a simple Jasmine test running """Helper class which allows you to create a simple Jasmine test.
through Selenium
Jasmine tests are run through Selenium.
To run a jasmine test suite, create a class which extends JasmineTests in To run a jasmine test suite, create a class which extends JasmineTests in
the :file:`horizon/test/jasmine/jasmine_tests.py` and define two class the :file:`horizon/test/jasmine/jasmine_tests.py` and define two class
attributes attributes

View File

@ -21,10 +21,12 @@ from horizon.test import helpers
class HackingTestCase(helpers.TestCase): class HackingTestCase(helpers.TestCase):
"""This class tests the hacking checks in horizon.hacking.checks by passing """This class tests the hacking checks in horizon.hacking.checks.
strings to the check methods like the pep8/flake8 parser would. The parser
loops over each line in the file and then passes the parameters to the Each check passes passing strings to the check methods like the
check method. The parameter names in the check method dictate what type of pep8/flake8 parser would. The parser loops over each line in the
file and then passes the parameters to the check method.
The parameter names in the check method dictate what type of
object is passed to the check method. The parameter types are: object is passed to the check method. The parameter types are:
logical_line logical_line

View File

@ -1,4 +1,4 @@
# -*- encoding: UTF-8 -*- # -*- encoding: utf-8 -*-
# Copyright 2015, Rackspace, US, Inc. # Copyright 2015, Rackspace, US, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -44,17 +44,14 @@ class ExtractAngularTestCase(test.TestCase):
messages) messages)
def test_attr_value(self): def test_attr_value(self):
"""We should not translate tags that have translate as the value of an """Should not translate tags with translate as the value of an attr."""
attribute.
"""
buf = StringIO('<html><div id="translate">hello world!</div></html>') buf = StringIO('<html><div id="translate">hello world!</div></html>')
messages = list(extract_angular(buf, [], [], {})) messages = list(extract_angular(buf, [], [], {}))
self.assertEqual([], messages) self.assertEqual([], messages)
def test_attr_value_plus_directive(self): def test_attr_value_plus_directive(self):
"""Unless they also have a translate directive. """Unless they also have a translate directive."""
"""
buf = StringIO( buf = StringIO(
'<html><div id="translate" translate>hello world!</div></html>') '<html><div id="translate" translate>hello world!</div></html>')

View File

@ -118,10 +118,11 @@ class BaseHorizonTests(test.TestCase):
dash.register(panel) dash.register(panel)
def _reload_urls(self): def _reload_urls(self):
"""Clears out the URL caches, reloads the root urls module, and """Clears out the URL caches, and reloads the root urls module.
re-triggers the autodiscovery mechanism for Horizon. Allows URLs
to be re-calculated after registering new dashboards. Useful It re-triggers the autodiscovery mechanism for Horizon.
only for testing and should never be used on a live site. Allows URLs to be re-calculated after registering new dashboards.
Useful only for testing and should never be used on a live site.
""" """
urlresolvers.clear_url_caches() urlresolvers.clear_url_caches()
moves.reload_module(import_module(settings.ROOT_URLCONF)) moves.reload_module(import_module(settings.ROOT_URLCONF))
@ -371,8 +372,9 @@ class GetUserHomeTests(BaseHorizonTests):
class CustomPanelTests(BaseHorizonTests): class CustomPanelTests(BaseHorizonTests):
"""Test customization of dashboards and panels """Test customization of dashboards and panels.
using 'customization_module' to HORIZON_CONFIG.
This tests customization using 'customization_module' to HORIZON_CONFIG.
""" """
def setUp(self): def setUp(self):
@ -407,8 +409,9 @@ class CustomPanelTests(BaseHorizonTests):
class CustomPermissionsTests(BaseHorizonTests): class CustomPermissionsTests(BaseHorizonTests):
"""Test customization of permissions on panels """Test customization of permissions on panels.
using 'customization_module' to HORIZON_CONFIG.
This tests customization using 'customization_module' to HORIZON_CONFIG.
""" """
def setUp(self): def setUp(self):

View File

@ -71,50 +71,42 @@ class FinderTests(unittest.TestCase):
# discover_files() # discover_files()
# #
def test_find_all(self): def test_find_all(self):
"""Find all files """Find all files"""
"""
files = fd.discover_files(base_path) files = fd.discover_files(base_path)
self.assertEqual(len(files), 18) self.assertEqual(len(files), 18)
def test_find_a(self): def test_find_a(self):
"""Find all files in folder `a` """Find all files in folder `a`"""
"""
files = fd.discover_files(base_path, sub_path='a') files = fd.discover_files(base_path, sub_path='a')
self.assertEqual(len(files), 8) self.assertEqual(len(files), 8)
def test_find_b(self): def test_find_b(self):
"""Find all files in folder `b` """Find all files in folder `b`"""
"""
files = fd.discover_files(base_path, sub_path='b') files = fd.discover_files(base_path, sub_path='b')
self.assertEqual(len(files), 10) self.assertEqual(len(files), 10)
def test_find_all_js(self): def test_find_all_js(self):
"""Find all files with extension of `.js` """Find all files with extension of `.js`"""
"""
files = fd.discover_files(base_path, ext='.js') files = fd.discover_files(base_path, ext='.js')
self.assertEqual(len(files), 14) self.assertEqual(len(files), 14)
def test_find_all_html(self): def test_find_all_html(self):
"""Find all files with extension of `.html` """Find all files with extension of `.html`"""
"""
files = fd.discover_files(base_path, ext='.html') files = fd.discover_files(base_path, ext='.html')
self.assertEqual(len(files), 2) self.assertEqual(len(files), 2)
def test_find_all_js_in_a(self): def test_find_all_js_in_a(self):
"""Find all files with extension of `.js` in folder a """Find all files with extension of `.js` in folder a"""
"""
files = fd.discover_files(base_path, sub_path='b', ext='.js') files = fd.discover_files(base_path, sub_path='b', ext='.js')
self.assertEqual(len(files), 7) self.assertEqual(len(files), 7)
def test_find_all_html_in_a(self): def test_find_all_html_in_a(self):
"""Find all files with extension of `.html` in folder a """Find all files with extension of `.html` in folder a"""
"""
files = fd.discover_files(base_path, sub_path='b', ext='.html') files = fd.discover_files(base_path, sub_path='b', ext='.html')
self.assertEqual(len(files), 1) self.assertEqual(len(files), 1)
def test_find_all_file_trim_base_path(self): def test_find_all_file_trim_base_path(self):
"""Find all files in folder, trim base path """Find all files in folder, trim base path"""
"""
files = fd.discover_files(base_path, sub_path='a', trim_base_path=True) files = fd.discover_files(base_path, sub_path='a', trim_base_path=True)
self.assertTrue(files[0].startswith('a/')) self.assertTrue(files[0].startswith('a/'))
@ -125,8 +117,7 @@ class FinderTests(unittest.TestCase):
# sort_js_files() # sort_js_files()
# #
def test_sort_js_files(self): def test_sort_js_files(self):
"""Sort all JavaScript files """Sort all JavaScript files"""
"""
files = fd.discover_files(base_path, ext='.js') files = fd.discover_files(base_path, ext='.js')
sources, mocks, specs = fd.sort_js_files(files) sources, mocks, specs = fd.sort_js_files(files)
@ -156,8 +147,7 @@ class FinderTests(unittest.TestCase):
# discover_static_files() # discover_static_files()
# #
def test_discover_all_static_files(self): def test_discover_all_static_files(self):
"""Find all static files """Find all static files"""
"""
sources, mocks, specs, templates = fd.discover_static_files(base_path) sources, mocks, specs, templates = fd.discover_static_files(base_path)
self.assertEqual(len(sources), 6) self.assertEqual(len(sources), 6)
self.assertEqual(len(mocks), 2) self.assertEqual(len(mocks), 2)
@ -189,8 +179,7 @@ class FinderTests(unittest.TestCase):
# populate_horizon_config() # populate_horizon_config()
# #
def test_populate_horizon_config(self): def test_populate_horizon_config(self):
"""Populate horizon config """Populate horizon config"""
"""
horizon_config = {} horizon_config = {}
fd.populate_horizon_config(horizon_config, base_path) fd.populate_horizon_config(horizon_config, base_path)

View File

@ -12,9 +12,7 @@
class ObjDictWrapper(dict): class ObjDictWrapper(dict):
"""ObjDictWrapper is a container that provides both dictionary-like and """Wrapper that provides both dict- and object-like attribute access."""
object-like attribute access.
"""
def __getattr__(self, item): def __getattr__(self, item):
if item in self: if item in self:
return self[item] return self[item]

View File

@ -109,8 +109,7 @@ class WebElementWrapper(WrapperFindOverride, webelement.WebElement):
class WebDriverWrapper(WrapperFindOverride, WebDriver): class WebDriverWrapper(WrapperFindOverride, WebDriver):
"""Wrapper for webdriver to return WebElementWrapper on find_element. """Wrapper for webdriver to return WebElementWrapper on find_element."""
"""
def reload_request(self, locator, index): def reload_request(self, locator, index):
try: try:
# element was found out via find_elements # element was found out via find_elements

View File

@ -87,7 +87,9 @@ def offline_context():
# A piece of middleware that stores the theme cookie value into # A piece of middleware that stores the theme cookie value into
# local thread storage so the template loader can access it # local thread storage so the template loader can access it
class ThemeMiddleware(object): class ThemeMiddleware(object):
"""The Theme Middleware component. The custom template loaders """The Theme Middleware component.
The custom template loaders
don't have access to the request object, so we need to store don't have access to the request object, so we need to store
the Cookie's theme value for use later in the Django chain. the Cookie's theme value for use later in the Django chain.
""" """
@ -111,7 +113,9 @@ class ThemeMiddleware(object):
class ThemeTemplateLoader(tLoaderCls): class ThemeTemplateLoader(tLoaderCls):
"""Themes can contain template overrides, so we need to check the """Theme-aware template loader.
Themes can contain template overrides, so we need to check the
theme directory first, before loading any of the standard templates. theme directory first, before loading any of the standard templates.
""" """
is_usable = True is_usable = True

View File

@ -1,4 +1,4 @@
# -*- encoding: UTF-8 -*- # -*- encoding: utf-8 -*-
# Copyright 2015, Rackspace, US, Inc. # Copyright 2015, Rackspace, US, Inc.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); you may # Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -144,9 +144,11 @@ class AngularGettextHTMLParser(html_parser.HTMLParser):
def extract_angular(fileobj, keywords, comment_tags, options): def extract_angular(fileobj, keywords, comment_tags, options):
"""Extract messages from angular template (HTML) files that use the """Extract messages from angular template (HTML) files.
It extract messages from angular template (HTML) files that use
angular-gettext translate directive as per angular-gettext translate directive as per
https://angular-gettext.rocketeer.be/ . https://angular-gettext.rocketeer.be/
:param fileobj: the file-like object the messages should be extracted :param fileobj: the file-like object the messages should be extracted
from from

View File

@ -23,8 +23,7 @@ SPEC_EXT = '.spec.js'
def discover_files(base_path, sub_path='', ext='', trim_base_path=False): def discover_files(base_path, sub_path='', ext='', trim_base_path=False):
"""Discovers all files with certain extension in given paths. """Discovers all files with certain extension in given paths."""
"""
file_list = [] file_list = []
for root, dirs, files in walk(path.join(base_path, sub_path)): for root, dirs, files in walk(path.join(base_path, sub_path)):
if trim_base_path: if trim_base_path:
@ -36,8 +35,10 @@ def discover_files(base_path, sub_path='', ext='', trim_base_path=False):
def sort_js_files(js_files): def sort_js_files(js_files):
"""Sorts JavaScript files in `js_files` into source files, mock files """Sorts JavaScript files in `js_files`.
and spec files based on file extension.
It sorts JavaScript files in a given `js_files`
into source files, mock files and spec files based on file extension.
Output: Output:
@ -72,8 +73,10 @@ def sort_js_files(js_files):
def discover_static_files(base_path, sub_path=''): def discover_static_files(base_path, sub_path=''):
"""Discovers static files in given paths, returning JavaScript sources, """Discovers static files in given paths.
mocks, specs and HTML templates, all grouped in lists.
It returns JavaScript sources, mocks, specs and HTML templates,
all grouped in lists.
""" """
js_files = discover_files(base_path, sub_path=sub_path, js_files = discover_files(base_path, sub_path=sub_path,
ext='.js', trim_base_path=True) ext='.js', trim_base_path=True)
@ -105,8 +108,7 @@ def populate_horizon_config(horizon_config, base_path,
def _log(file_list, list_name, in_path): def _log(file_list, list_name, in_path):
"""Logs result at debug level """Logs result at debug level"""
"""
file_names = '\n'.join(file_list) file_names = '\n'.join(file_list)
LOG.debug("\nDiscovered %(size)d %(name)s file(s) in %(path)s:\n" LOG.debug("\nDiscovered %(size)d %(name)s file(s) in %(path)s:\n"
"%(files)s\n", "%(files)s\n",

View File

@ -30,9 +30,8 @@ def replace_underscores(string):
@register.filter @register.filter
def parse_isotime(timestr, default=None): def parse_isotime(timestr, default=None):
"""This duplicates oslo timeutils parse_isotime but with a # This duplicates oslo timeutils parse_isotime but with a
@register.filter annotation and a silent fallback on error. # @register.filter annotation and a silent fallback on error.
"""
try: try:
return iso8601.parse_date(timestr) return iso8601.parse_date(timestr)
except (iso8601.ParseError, TypeError): except (iso8601.ParseError, TypeError):
@ -41,8 +40,10 @@ def parse_isotime(timestr, default=None):
@register.filter @register.filter
def timesince_or_never(dt, default=None): def timesince_or_never(dt, default=None):
"""Call the Django ``timesince`` filter, but return the string """Call the Django ``timesince`` filter or a given default string.
*default* if *dt* is not a valid ``date`` or ``datetime`` object.
It returns the string *default* if *dt* is not a valid ``date``
or ``datetime`` object.
When *default* is None, "Never" is returned. When *default* is None, "Never" is returned.
""" """
if default is None: if default is None:

View File

@ -68,6 +68,7 @@ def logout_with_message(request, msg, redirect=True, status='success'):
def get_config_value(request, key, default, search_in_settings=True): def get_config_value(request, key, default, search_in_settings=True):
"""Retrieves the value of `key` from configuration in the following order: """Retrieves the value of `key` from configuration in the following order:
- from the session; if not found there then - from the session; if not found there then
- from cookies; if not found there then - from cookies; if not found there then
- from the settings file if `search_in_settings` is True, - from the settings file if `search_in_settings` is True,
@ -92,8 +93,7 @@ def get_config_value(request, key, default, search_in_settings=True):
def save_config_value(request, response, key, value): def save_config_value(request, response, key, value):
"""Sets value of key `key` to `value` in both session and cookies. """Sets value of key `key` to `value` in both session and cookies."""
"""
request.session[key] = value request.session[key] = value
response.set_cookie(key, value, expires=one_year_from_now()) response.set_cookie(key, value, expires=one_year_from_now())
return response return response
@ -126,14 +126,18 @@ def natural_sort(attr):
def get_keys(tuple_of_tuples): def get_keys(tuple_of_tuples):
"""Processes a tuple of 2-element tuples and returns a tuple containing """Returns a tuple containing first component of each tuple.
It processes a tuple of 2-element tuples and returns a tuple containing
first component of each tuple. first component of each tuple.
""" """
return tuple([t[0] for t in tuple_of_tuples]) return tuple([t[0] for t in tuple_of_tuples])
def value_for_key(tuple_of_tuples, key): def value_for_key(tuple_of_tuples, key):
"""Processes a tuple of 2-element tuples and returns the value """Returns a value containing to the given key.
It processes a tuple of 2-element tuples and returns the value
corresponding to the given key. If no value is found, the key is returned. corresponding to the given key. If no value is found, the key is returned.
""" """
for t in tuple_of_tuples: for t in tuple_of_tuples:
@ -144,7 +148,9 @@ def value_for_key(tuple_of_tuples, key):
def next_key(tuple_of_tuples, key): def next_key(tuple_of_tuples, key):
"""Processes a tuple of 2-element tuples and returns the key which comes """Returns the key which comes after the given key.
It processes a tuple of 2-element tuples and returns the key which comes
after the given key. after the given key.
""" """
for i, t in enumerate(tuple_of_tuples): for i, t in enumerate(tuple_of_tuples):
@ -156,7 +162,9 @@ def next_key(tuple_of_tuples, key):
def previous_key(tuple_of_tuples, key): def previous_key(tuple_of_tuples, key):
"""Processes a tuple of 2-element tuples and returns the key which comes """Returns the key which comes before the give key.
It Processes a tuple of 2-element tuples and returns the key which comes
before the given key. before the given key.
""" """
for i, t in enumerate(tuple_of_tuples): for i, t in enumerate(tuple_of_tuples):
@ -168,8 +176,9 @@ def previous_key(tuple_of_tuples, key):
def format_value(value): def format_value(value):
"""Returns the given value rounded to one decimal place if it is a """Returns the given value rounded to one decimal place if deciaml.
decimal, or integer if it is an integer.
Returns the integer if an integer is given.
""" """
value = decimal.Decimal(str(value)) value = decimal.Decimal(str(value))
if int(value) == value: if int(value) == value:

View File

@ -22,21 +22,21 @@ class HTMLElement(object):
self.classes = getattr(self, "classes", []) self.classes = getattr(self, "classes", [])
def get_default_classes(self): def get_default_classes(self):
"""Returns an iterable of default classes which should be combined with """Returns an iterable of default classes.
any other declared classes.
They will be combined with any other declared classes.
""" """
return [] return []
def get_default_attrs(self): def get_default_attrs(self):
"""Returns a dict of default attributes which should be combined with """Returns a dict of default attributes.
other declared attributes.
They will be combined with other declared attributes.
""" """
return {} return {}
def get_final_attrs(self, classes=True): def get_final_attrs(self, classes=True):
"""Returns a dict containing the final attributes of this element """Returns a dict containing the final attributes to be rendered."""
which will be rendered.
"""
final_attrs = copy.copy(self.get_default_attrs()) final_attrs = copy.copy(self.get_default_attrs())
final_attrs.update(self.attrs) final_attrs.update(self.attrs)
if classes: if classes:
@ -57,14 +57,18 @@ class HTMLElement(object):
@property @property
def attr_string(self): def attr_string(self):
"""Returns a flattened string of HTML attributes based on the """Returns a flattened string of HTML attributes.
HTML attributes are flattened based on the
``attrs`` dict provided to the class. ``attrs`` dict provided to the class.
""" """
return flatatt(self.get_final_attrs()) return flatatt(self.get_final_attrs())
@property @property
def attr_string_nc(self): def attr_string_nc(self):
"""Returns a flattened string of HTML attributes based on the """Returns a flattened string of HTML attributes.
HTML attributes are flattened based on the
``attrs`` dict provided to the class. ``attrs`` dict provided to the class.
""" """
return flatatt(self.get_final_attrs(False)) return flatatt(self.get_final_attrs(False))

View File

@ -33,9 +33,7 @@ ureg = pint.UnitRegistry()
def is_supported(unit): def is_supported(unit):
"""Returns a bool indicating whether the unit specified is supported by """Returns a bool indicating whether the unit specified is supported."""
this module.
"""
return unit in functions.get_keys(INFORMATION_UNITS) + TIME_UNITS return unit in functions.get_keys(INFORMATION_UNITS) + TIME_UNITS
@ -56,10 +54,11 @@ def is_larger(unit_1, unit_2):
def convert(value, source_unit, target_unit, fmt=False): def convert(value, source_unit, target_unit, fmt=False):
"""Converts value from source_unit to target_unit. Returns a tuple """Converts value from source_unit to target_unit.
containing the converted value and target_unit. Having fmt set to True
causes the value to be formatted to 1 decimal digit if it's a decimal or Returns a tuple containing the converted value and target_unit.
be formatted as integer if it's an integer. Having fmt set to True causes the value to be formatted to 1 decimal digit
if it's a decimal or be formatted as integer if it's an integer.
E.g: E.g:
@ -82,6 +81,7 @@ def convert(value, source_unit, target_unit, fmt=False):
def normalize(value, unit): def normalize(value, unit):
"""Converts the value so that it belongs to some expected range. """Converts the value so that it belongs to some expected range.
Returns the new value and new unit. Returns the new value and new unit.
E.g: E.g:

View File

@ -44,8 +44,10 @@ class PageTitleMixin(object):
page_title = "" page_title = ""
def render_context_with_title(self, context): def render_context_with_title(self, context):
"""This function takes in a context dict and uses it to render the """Render a page title and insert it into the context.
page_title variable, it then appends this title to the context using
This function takes in a context dict and uses it to render the
page_title variable. It then appends this title to the context using
the 'page_title' key. If there is already a page_title key defined in the 'page_title' key. If there is already a page_title key defined in
context received then this function will do nothing. context received then this function will do nothing.
""" """
@ -59,8 +61,10 @@ class PageTitleMixin(object):
return context return context
def render_to_response(self, context): def render_to_response(self, context):
"""This is an override of the default render_to_response function that """render_to_response() with a page title.
exists in the django generic views, this is here to inject the
This is an override of the default render_to_response function that
exists in the django generic views. This is here to inject the
page title into the context before the main template is rendered. page title into the context before the main template is rendered.
""" """
@ -105,7 +109,9 @@ class APIView(HorizonTemplateView):
""" """
def get_data(self, request, context, *args, **kwargs): def get_data(self, request, context, *args, **kwargs):
"""This method should handle any necessary API calls, update the """Load necessary API data into the context.
This method should handle any necessary API calls, update the
context object, and return the context object at the end. context object, and return the context object at the end.
""" """
return context return context

View File

@ -79,11 +79,11 @@ class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass):
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
@six.add_metaclass(ActionMetaclass) @six.add_metaclass(ActionMetaclass)
class Action(forms.Form): class Action(forms.Form):
"""An ``Action`` represents an atomic logical interaction you can have with """An ``Action`` represents an atomic logical interaction with the system.
the system. This is easier to understand with a conceptual example: in the
context of a "launch instance" workflow, actions would include "naming This is easier to understand with a conceptual example: in the context of
the instance", "selecting an image", and ultimately "launching the a "launch instance" workflow, actions would include "naming the instance",
instance". "selecting an image", and ultimately "launching the instance".
Because ``Actions`` are always interactive, they always provide form Because ``Actions`` are always interactive, they always provide form
controls, and thus inherit from Django's ``Form`` class. However, they controls, and thus inherit from Django's ``Form`` class. However, they
@ -192,9 +192,10 @@ class Action(forms.Form):
self.errors[NON_FIELD_ERRORS] = self.error_class([message]) self.errors[NON_FIELD_ERRORS] = self.error_class([message])
def handle(self, request, context): def handle(self, request, context):
"""Handles any requisite processing for this action. The method should """Handles any requisite processing for this action.
return either ``None`` or a dictionary of data to be passed to
:meth:`~horizon.workflows.Step.contribute`. The method should return either ``None`` or a dictionary of data
to be passed to :meth:`~horizon.workflows.Step.contribute`.
Returns ``None`` by default, effectively making it a no-op. Returns ``None`` by default, effectively making it a no-op.
""" """
@ -216,8 +217,9 @@ class MembershipAction(Action):
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
class Step(object): class Step(object):
"""A step is a wrapper around an action which defines its context in a """A wrapper around an action which defines its context in a workflow.
workflow. It knows about details such as:
It knows about details such as:
* The workflow's context data (data passed from step to step). * The workflow's context data (data passed from step to step).
@ -399,8 +401,9 @@ class Step(object):
return self._action return self._action
def prepare_action_context(self, request, context): def prepare_action_context(self, request, context):
"""Allows for customization of how the workflow context is passed to """Hook to customize how the workflow context is passed to the action.
the action; this is the reverse of what "contribute" does to make the
This is the reverse of what "contribute" does to make the
action outputs sane for the workflow. Changes to the context are not action outputs sane for the workflow. Changes to the context are not
saved globally here. They are localized to the action. saved globally here. They are localized to the action.
@ -430,8 +433,9 @@ class Step(object):
return True return True
def contribute(self, data, context): def contribute(self, data, context):
"""Adds the data listed in ``contributes`` to the workflow's shared """Adds the data listed in ``contributes`` to the workflow's context.
context. By default, the context is simply updated with all the data
By default, the context is simply updated with all the data
returned by the action. returned by the action.
Note that even if the value of one of the ``contributes`` keys is Note that even if the value of one of the ``contributes`` keys is
@ -524,9 +528,10 @@ class UpdateMembersStep(Step):
@six.python_2_unicode_compatible @six.python_2_unicode_compatible
@six.add_metaclass(WorkflowMetaclass) @six.add_metaclass(WorkflowMetaclass)
class Workflow(html.HTMLElement): class Workflow(html.HTMLElement):
"""A Workflow is a collection of Steps. Its interface is very """A Workflow is a collection of Steps.
straightforward, but it is responsible for handling some very
important tasks such as: Its interface is very straightforward, but it is responsible for handling
some very important tasks such as:
* Handling the injection, removal, and ordering of arbitrary steps. * Handling the injection, removal, and ordering of arbitrary steps.
@ -777,8 +782,7 @@ class Workflow(html.HTMLElement):
@classmethod @classmethod
def unregister(cls, step_class): def unregister(cls, step_class):
"""Unregisters a :class:`~horizon.workflows.Step` from the workflow. """Unregisters a :class:`~horizon.workflows.Step` from the workflow."""
"""
try: try:
cls._cls_registry.remove(step_class) cls._cls_registry.remove(step_class)
except KeyError: except KeyError:
@ -786,14 +790,17 @@ class Workflow(html.HTMLElement):
return cls._unregister(step_class) return cls._unregister(step_class)
def validate(self, context): def validate(self, context):
"""Hook for custom context data validation. Should return a boolean """Hook for custom context data validation.
value or raise :class:`~horizon.exceptions.WorkflowValidationError`.
Should return a booleanvalue or
raise :class:`~horizon.exceptions.WorkflowValidationError`.
""" """
return True return True
def is_valid(self): def is_valid(self):
"""Verified that all required data is present in the context and """Verifies that all required data is present in the context.
calls the ``validate`` method to allow for finer-grained checks
It also calls the ``validate`` method to allow for finer-grained checks
on the context data. on the context data.
""" """
missing = self.depends_on - set(self.context.keys()) missing = self.depends_on - set(self.context.keys())
@ -814,10 +821,12 @@ class Workflow(html.HTMLElement):
return self.validate(self.context) return self.validate(self.context)
def finalize(self): def finalize(self):
"""Finalizes a workflow by running through all the actions in order """Finalizes a workflow by running through all the actions.
and calling their ``handle`` methods. Returns ``True`` on full success,
or ``False`` for a partial success, e.g. there were non-critical It runs all the actions in order and calling their ``handle`` methods.
errors. (If it failed completely the function wouldn't return.) Returns ``True`` on full success, or ``False`` for a partial success,
e.g. there were non-critical errors.
(If it failed completely the function wouldn't return.)
""" """
partial = False partial = False
for step in self.steps: for step in self.steps:
@ -837,16 +846,18 @@ class Workflow(html.HTMLElement):
return not partial return not partial
def handle(self, request, context): def handle(self, request, context):
"""Handles any final processing for this workflow. Should return a """Handles any final processing for this workflow.
boolean value indicating success.
Should return a boolean value indicating success.
""" """
return True return True
def get_success_url(self): def get_success_url(self):
"""Returns a URL to redirect the user to upon completion. By default it """Returns a URL to redirect the user to upon completion.
will attempt to parse a ``success_url`` attribute on the workflow,
which can take the form of a reversible URL pattern name, or a By default it will attempt to parse a ``success_url`` attribute on the
standard HTTP URL. workflow, which can take the form of a reversible URL pattern name,
or a standard HTTP URL.
""" """
try: try:
return urlresolvers.reverse(self.success_url) return urlresolvers.reverse(self.success_url)
@ -854,8 +865,10 @@ class Workflow(html.HTMLElement):
return self.success_url return self.success_url
def format_status_message(self, message): def format_status_message(self, message):
"""Hook to allow customization of the message returned to the user """Hook to allow customization of the message returned to the user.
upon successful or unsuccessful completion of the workflow.
This is called upon both successful or unsuccessful completion of
the workflow.
By default it simply inserts the workflow's name into the message By default it simply inserts the workflow's name into the message
string. string.
@ -894,10 +907,13 @@ class Workflow(html.HTMLElement):
return self.request.get_full_path().partition('?')[0] return self.request.get_full_path().partition('?')[0]
def add_error_to_step(self, message, slug): def add_error_to_step(self, message, slug):
"""Adds an error to the workflow's Step with the """Adds an error message to the workflow's Step.
specified slug based on API issues. This is useful
when you wish for API errors to appear as errors on This is useful when you wish for API errors to appear as errors
the form rather than using the messages framework. on the form rather than using the messages framework.
The workflow's Step is specified by its slug.
""" """
step = self.get_step(slug) step = self.get_step(slug)
if step: if step:

View File

@ -29,8 +29,7 @@ from horizon import messages
class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView): class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
"""A generic class-based view which handles the intricacies of workflow """A generic view which handles the intricacies of workflow processing.
processing with minimal user configuration.
.. attribute:: workflow_class .. attribute:: workflow_class
@ -67,8 +66,10 @@ class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
"on %s." % self.__class__.__name__) "on %s." % self.__class__.__name__)
def get_initial(self): def get_initial(self):
"""Returns initial data for the workflow. Defaults to using the GET """Returns initial data for the workflow.
parameters to allow pre-seeding of the workflow context values.
Defaults to using the GET parameters
to allow pre-seeding of the workflow context values.
""" """
return copy.copy(self.request.GET) return copy.copy(self.request.GET)
@ -102,8 +103,10 @@ class WorkflowView(hz_views.ModalBackdropMixin, generic.TemplateView):
return context return context
def get_layout(self): def get_layout(self):
"""returns classes for the workflow element in template based on """Returns classes for the workflow element in template.
the workflow characteristics
The returned classes are determied based on
the workflow characteristics.
""" """
if self.request.is_ajax(): if self.request.is_ajax():
layout = ['modal', ] layout = ['modal', ]