diff options
author | Tomas Sedovic <tomas@sedovic.cz> | 2013-08-09 14:43:41 +0200 |
---|---|---|
committer | Tomas Sedovic <tomas@sedovic.cz> | 2013-08-09 14:43:41 +0200 |
commit | 1b8705bedfd167e7636907ce2ce31d730ac9c119 (patch) | |
tree | ee67a2a1b20bc6fdbbd1b30d8ac8dcd406e594fe | |
parent | 1839264f2a0b7afa69209d1a6a4d3041fa6cde96 (diff) | |
download | tuskar-ui-1b8705bedfd167e7636907ce2ce31d730ac9c119.tar.gz |
Fix the Resource Class creation error
Had to clean up some of the horizon.workflows code we were carrying around.
Signed-off-by: Tomas Sedovic <tomas@sedovic.cz>
-rw-r--r-- | tuskar_ui/workflows.py | 830 |
1 files changed, 15 insertions, 815 deletions
diff --git a/tuskar_ui/workflows.py b/tuskar_ui/workflows.py index 60d8a1e9..45993887 100644 --- a/tuskar_ui/workflows.py +++ b/tuskar_ui/workflows.py @@ -36,412 +36,14 @@ from horizon import base from horizon import exceptions from horizon.templatetags.horizon import has_permissions from horizon.utils import html +import horizon.workflows LOG = logging.getLogger(__name__) -class WorkflowContext(dict): - def __init__(self, workflow, *args, **kwargs): - super(WorkflowContext, self).__init__(*args, **kwargs) - self._workflow = workflow - - def __setitem__(self, key, val): - super(WorkflowContext, self).__setitem__(key, val) - return self._workflow._trigger_handlers(key) - - def __delitem__(self, key): - return self.__setitem__(key, None) - - def set(self, key, val): - return self.__setitem__(key, val) - - def unset(self, key): - return self.__delitem__(key) - - -class ActionMetaclass(forms.forms.DeclarativeFieldsMetaclass): - def __new__(mcs, name, bases, attrs): - # Pop Meta for later processing - opts = attrs.pop("Meta", None) - # Create our new class - cls = super(ActionMetaclass, mcs).__new__(mcs, name, bases, attrs) - # Process options from Meta - cls.name = getattr(opts, "name", name) - cls.slug = getattr(opts, "slug", slugify(name)) - cls.permissions = getattr(opts, "permissions", ()) - cls.progress_message = getattr(opts, - "progress_message", - _("Processing...")) - cls.help_text = getattr(opts, "help_text", "") - cls.help_text_template = getattr(opts, "help_text_template", None) - return cls - - -class Action(forms.Form): - """ - An ``Action`` represents an atomic logical interaction you can have with - the system. This is easier to understand with a conceptual example: in the - context of a "launch instance" workflow, actions would include "naming - the instance", "selecting an image", and ultimately "launching the - instance". - - Because ``Actions`` are always interactive, they always provide form - controls, and thus inherit from Django's ``Form`` class. However, they - have some additional intelligence added to them: - - * ``Actions`` are aware of the permissions required to complete them. - - * ``Actions`` have a meta-level concept of "help text" which is meant to be - displayed in such a way as to give context to the action regardless of - where the action is presented in a site or workflow. - - * ``Actions`` understand how to handle their inputs and produce outputs, - much like :class:`~horizon.forms.SelfHandlingForm` does now. - - ``Action`` classes may define the following attributes in a ``Meta`` - class within them: - - .. attribute:: name - - The verbose name for this action. Defaults to the name of the class. - - .. attribute:: slug - - A semi-unique slug for this action. Defaults to the "slugified" name - of the class. - - .. attribute:: permissions - - A list of permission names which this action requires in order to be - completed. Defaults to an empty list (``[]``). - - .. attribute:: help_text - - A string of simple help text to be displayed alongside the Action's - fields. - - .. attribute:: help_text_template - - A path to a template which contains more complex help text to be - displayed alongside the Action's fields. In conjunction with - :meth:`~horizon.workflows.Action.get_help_text` method you can - customize your help text template to display practically anything. - """ - - __metaclass__ = ActionMetaclass - - def __init__(self, request, context, *args, **kwargs): - if request.method == "POST": - super(Action, self).__init__(request.POST, initial=context) - else: - super(Action, self).__init__(initial=context) - - if not hasattr(self, "handle"): - raise AttributeError("The action %s must define a handle method." - % self.__class__.__name__) - self.request = request - self._populate_choices(request, context) - - def __unicode__(self): - return force_unicode(self.name) - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self.slug) - - def _populate_choices(self, request, context): - for field_name, bound_field in self.fields.items(): - meth = getattr(self, "populate_%s_choices" % field_name, None) - if meth is not None and callable(meth): - bound_field.choices = meth(request, context) - - def get_help_text(self, extra_context=None): - """ Returns the help text for this step. """ - text = "" - extra_context = extra_context or {} - if self.help_text_template: - tmpl = template.loader.get_template(self.help_text_template) - context = template.RequestContext(self.request, extra_context) - text += tmpl.render(context) - else: - text += linebreaks(force_unicode(self.help_text)) - return safe(text) - - def add_error(self, message): - """ - Adds an error to the Action's Step based on API issues. - """ - self._get_errors()[NON_FIELD_ERRORS] = self.error_class([message]) - - def handle(self, request, context): - """ - Handles any requisite processing for this action. 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. - """ - return None - - -class Step(object): - """ - A step is a wrapper around an action which defines it's context in a - workflow. It knows about details such as: - - * The workflow's context data (data passed from step to step). - - * The data which must be present in the context to begin this step (the - step's dependencies). - - * The keys which will be added to the context data upon completion of the - step. - - * The connections between this step's fields and changes in the context - data (e.g. if that piece of data changes, what needs to be updated in - this step). - - A ``Step`` class has the following attributes: - - .. attribute:: action - - The :class:`~horizon.workflows.Action` class which this step wraps. - - .. attribute:: depends_on - - A list of context data keys which this step requires in order to - begin interaction. - - .. attribute:: contributes - - A list of keys which this step will contribute to the workflow's - context data. Optional keys should still be listed, even if their - values may be set to ``None``. - - .. attribute:: connections - - A dictionary which maps context data key names to lists of callbacks. - The callbacks may be functions, dotted python paths to functions - which may be imported, or dotted strings beginning with ``"self"`` - to indicate methods on the current ``Step`` instance. - - .. attribute:: before - - Another ``Step`` class. This optional attribute is used to provide - control over workflow ordering when steps are dynamically added to - workflows. The workflow mechanism will attempt to place the current - step before the step specified in the attribute. - - .. attribute:: after - - Another ``Step`` class. This attribute has the same purpose as - :meth:`~horizon.workflows.Step.before` except that it will instead - attempt to place the current step after the given step. - - .. attribute:: help_text - - A string of simple help text which will be prepended to the ``Action`` - class' help text if desired. - - .. attribute:: template_name - - A path to a template which will be used to render this step. In - general the default common template should be used. Default: - ``"horizon/common/_workflow_step.html"``. - - .. attribute:: has_errors - - A boolean value which indicates whether or not this step has any - errors on the action within it or in the scope of the workflow. This - attribute will only accurately reflect this status after validation - has occurred. - - .. attribute:: slug - - Inherited from the ``Action`` class. - - .. attribute:: name - - Inherited from the ``Action`` class. - - .. attribute:: permissions - - Inherited from the ``Action`` class. - """ - action_class = None - depends_on = () - contributes = () - connections = None - before = None - after = None - help_text = "" - template_name = "horizon/common/_workflow_step.html" - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self.slug) - - def __unicode__(self): - return force_unicode(self.name) - - def __init__(self, workflow): - super(Step, self).__init__() - self.workflow = workflow - - cls = self.__class__.__name__ - if not (self.action_class and issubclass(self.action_class, Action)): - raise AttributeError("You must specify an action for %s." % cls) - - self.slug = self.action_class.slug - self.name = self.action_class.name - self.permissions = self.action_class.permissions - self.has_errors = False - self._handlers = {} - - if self.connections is None: - # We want a dict, but don't want to declare a mutable type on the - # class directly. - self.connections = {} - - # Gather our connection handlers and make sure they exist. - for key, handlers in self.connections.items(): - self._handlers[key] = [] - # TODO(gabriel): This is a poor substitute for broader handling - if not isinstance(handlers, (list, tuple)): - raise TypeError("The connection handlers for %s must be a " - "list or tuple." % cls) - for possible_handler in handlers: - if callable(possible_handler): - # If it's callable we know the function exists and is valid - self._handlers[key].append(possible_handler) - continue - elif not isinstance(possible_handler, basestring): - return TypeError("Connection handlers must be either " - "callables or strings.") - bits = possible_handler.split(".") - if bits[0] == "self": - root = self - for bit in bits[1:]: - try: - root = getattr(root, bit) - except AttributeError: - raise AttributeError("The connection handler %s " - "could not be found on %s." - % (possible_handler, cls)) - handler = root - elif len(bits) == 1: - # Import by name from local module not supported - raise ValueError("Importing a local function as a string " - "is not supported for the connection " - "handler %s on %s." - % (possible_handler, cls)) - else: - # Try a general import - module_name = ".".join(bits[:-1]) - try: - mod = import_module(module_name) - handler = getattr(mod, bits[-1]) - except ImportError: - raise ImportError("Could not import %s from the " - "module %s as a connection " - "handler on %s." - % (bits[-1], module_name, cls)) - except AttributeError: - raise AttributeError("Could not import %s from the " - "module %s as a connection " - "handler on %s." - % (bits[-1], module_name, cls)) - self._handlers[key].append(handler) - - @property - def action(self): - if not getattr(self, "_action", None): - try: - # Hook in the action context customization. - workflow_context = dict(self.workflow.context) - context = self.prepare_action_context(self.workflow.request, - workflow_context) - self._action = self.action_class(self.workflow.request, - context) - except: - LOG.exception("Problem instantiating action class.") - raise - return self._action - - def prepare_action_context(self, request, context): - """ - Allows for customization of how the workflow context is passed to the - action; this is the reverse of what "contribute" does to make the - action outputs sane for the workflow. Changes to the context are not - saved globally here. They are localized to the action. - - Simply returns the unaltered context by default. - """ - return context - - def get_id(self): - """ Returns the ID for this step. Suitable for use in HTML markup. """ - return "%s__%s" % (self.workflow.slug, self.slug) - - def _verify_contributions(self, context): - for key in self.contributes: - # Make sure we don't skip steps based on weird behavior of - # POST query dicts. - field = self.action.fields.get(key, None) - if field and field.required and not context.get(key): - context.pop(key, None) - failed_to_contribute = set(self.contributes) - failed_to_contribute -= set(context.keys()) - if failed_to_contribute: - raise exceptions.WorkflowError("The following expected data was " - "not added to the workflow context " - "by the step %s: %s." - % (self.__class__, - failed_to_contribute)) - return True - - def contribute(self, data, context): - """ - Adds the data listed in ``contributes`` to the workflow's shared - context. By default, the context is simply updated with all the data - returned by the action. - - Note that even if the value of one of the ``contributes`` keys is - not present (e.g. optional) the key should still be added to the - context with a value of ``None``. - """ - if data: - for key in self.contributes: - context[key] = data.get(key, None) - return context - - def render(self): - """ Renders the step. """ - step_template = template.loader.get_template(self.template_name) - extra_context = {"form": self.action, - "step": self} - - # FIXME: TableStep: - if issubclass(self.__class__, TableStep): - extra_context.update(self.get_context_data(self.workflow.request)) - - context = template.RequestContext(self.workflow.request, extra_context) - return step_template.render(context) - - def get_help_text(self): - """ Returns the help text for this step. """ - text = linebreaks(force_unicode(self.help_text)) - text += self.action.get_help_text() - return safe(text) - - def add_error(self, message): - """ - Adds an error to the Step based on API issues. - """ - self.action.add_error(message) - - # FIXME: TableStep -class TableStep(Step): +class TableStep(horizon.workflows.Step): """ A :class:`~horizon.workflows.Step` class which knows how to deal with :class:`~horizon.tables.DataTable` classes rendered inside of it. @@ -475,6 +77,19 @@ class TableStep(Step): self._tables = SortedDict(table_instances) self._table_data_loaded = False + def render(self): + """ Renders the step. """ + step_template = template.loader.get_template(self.template_name) + extra_context = {"form": self.action, + "step": self} + + # FIXME: TableStep: + if issubclass(self.__class__, TableStep): + extra_context.update(self.get_context_data(self.workflow.request)) + + context = template.RequestContext(self.workflow.request, extra_context) + return step_template.render(context) + def load_table_data(self): """ Calls the ``get_{{ table_name }}_data`` methods for each table class @@ -517,418 +132,3 @@ class TableStep(Step): def has_more_data(self, table): return False - - -class WorkflowMetaclass(type): - def __new__(mcs, name, bases, attrs): - super(WorkflowMetaclass, mcs).__new__(mcs, name, bases, attrs) - attrs["_cls_registry"] = set([]) - return type.__new__(mcs, name, bases, attrs) - - -class UpdateMembersStep(Step): - """A step that allows a user to add/remove members from a group. - - .. attribute:: show_roles - - Set to False to disable the display of the roles dropdown. - - .. attribute:: available_list_title - - The title used for the available list column. - - .. attribute:: members_list_title - - The title used for the members list column. - - .. attribute:: no_available_text - - The placeholder text used when the available list is empty. - - .. attribute:: no_members_text - - The placeholder text used when the members list is empty. - - """ - template_name = "horizon/common/_workflow_step_update_members.html" - show_roles = True - available_list_title = _("All available") - members_list_title = _("Members") - no_available_text = _("None available.") - no_members_text = _("No members.") - - -class Workflow(html.HTMLElement): - """ - A Workflow is a collection of Steps. It's 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. - - * Determining if the workflow can be completed by a given user at runtime - based on all available information. - - * Dispatching connections between steps to ensure that when context data - changes all the applicable callback functions are executed. - - * Verifying/validating the overall data integrity and subsequently - triggering the final method to complete the workflow. - - The ``Workflow`` class has the following attributes: - - .. attribute:: name - - The verbose name for this workflow which will be displayed to the user. - Defaults to the class name. - - .. attribute:: slug - - The unique slug for this workflow. Required. - - .. attribute:: steps - - Read-only access to the final ordered set of step instances for - this workflow. - - .. attribute:: default_steps - - A list of :class:`~horizon.workflows.Step` classes which serve as the - starting point for this workflow's ordered steps. Defaults to an empty - list (``[]``). - - .. attribute:: finalize_button_name - - The name which will appear on the submit button for the workflow's - form. Defaults to ``"Save"``. - - .. attribute:: success_message - - A string which will be displayed to the user upon successful completion - of the workflow. Defaults to - ``"{{ workflow.name }} completed successfully."`` - - .. attribute:: failure_message - - A string which will be displayed to the user upon failure to complete - the workflow. Defaults to ``"{{ workflow.name }} did not complete."`` - - .. attribute:: depends_on - - A roll-up list of all the ``depends_on`` values compiled from the - workflow's steps. - - .. attribute:: contributions - - A roll-up list of all the ``contributes`` values compiled from the - workflow's steps. - - .. attribute:: template_name - - Path to the template which should be used to render this workflow. - In general the default common template should be used. Default: - ``"horizon/common/_workflow.html"``. - - .. attribute:: entry_point - - The slug of the step which should initially be active when the - workflow is rendered. This can be passed in upon initialization of - the workflow, or set anytime after initialization but before calling - either ``get_entry_point`` or ``render``. - - .. attribute:: redirect_param_name - - The name of a parameter used for tracking the URL to redirect to upon - completion of the workflow. Defaults to ``"next"``. - - .. attribute:: object - - The object (if any) which this workflow relates to. In the case of - a workflow which creates a new resource the object would be the created - resource after the relevant creation steps have been undertaken. In - the case of a workflow which updates a resource it would be the - resource being updated after it has been retrieved. - - """ - __metaclass__ = WorkflowMetaclass - slug = None - default_steps = () - template_name = "horizon/common/_workflow.html" - finalize_button_name = _("Save") - success_message = _("%s completed successfully.") - failure_message = _("%s did not complete.") - redirect_param_name = "next" - multipart = False - _registerable_class = Step - - def __unicode__(self): - return self.name - - def __repr__(self): - return "<%s: %s>" % (self.__class__.__name__, self.slug) - - def __init__(self, request=None, context_seed=None, entry_point=None, - *args, **kwargs): - super(Workflow, self).__init__(*args, **kwargs) - if self.slug is None: - raise AttributeError("The workflow %s must have a slug." - % self.__class__.__name__) - self.name = getattr(self, "name", self.__class__.__name__) - self.request = request - self.depends_on = set([]) - self.contributions = set([]) - self.entry_point = entry_point - self.object = None - - # Put together our steps in order. Note that we pre-register - # non-default steps so that we can identify them and subsequently - # insert them in order correctly. - self._registry = dict([(step_class, step_class(self)) for step_class - in self.__class__._cls_registry - if step_class not in self.default_steps]) - self._gather_steps() - - # Determine all the context data we need to end up with. - for step in self.steps: - self.depends_on = self.depends_on | set(step.depends_on) - self.contributions = self.contributions | set(step.contributes) - - # Initialize our context. For ease we can preseed it with a - # regular dictionary. This should happen after steps have been - # registered and ordered. - self.context = WorkflowContext(self) - context_seed = context_seed or {} - clean_seed = dict([(key, val) - for key, val in context_seed.items() - if key in self.contributions | self.depends_on]) - self.context_seed = clean_seed - self.context.update(clean_seed) - - if request and request.method == "POST": - for step in self.steps: - valid = step.action.is_valid() - # Be sure to use the CLEANED data if the workflow is valid. - if valid: - data = step.action.cleaned_data - else: - data = request.POST - self.context = step.contribute(data, self.context) - - @property - def steps(self): - if getattr(self, "_ordered_steps", None) is None: - self._gather_steps() - return self._ordered_steps - - def get_step(self, slug): - """ Returns the instantiated step matching the given slug. """ - for step in self.steps: - if step.slug == slug: - return step - - def _gather_steps(self): - ordered_step_classes = self._order_steps() - for default_step in self.default_steps: - self.register(default_step) - self._registry[default_step] = default_step(self) - self._ordered_steps = [self._registry[step_class] - for step_class in ordered_step_classes - if has_permissions(self.request.user, - self._registry[step_class])] - - def _order_steps(self): - steps = list(copy.copy(self.default_steps)) - additional = self._registry.keys() - for step in additional: - try: - min_pos = steps.index(step.after) - except ValueError: - min_pos = 0 - try: - max_pos = steps.index(step.before) - except ValueError: - max_pos = len(steps) - if min_pos > max_pos: - raise exceptions.WorkflowError("The step %(new)s can't be " - "placed between the steps " - "%(after)s and %(before)s; the " - "step %(before)s comes before " - "%(after)s." - % {"new": additional, - "after": step.after, - "before": step.before}) - steps.insert(max_pos, step) - return steps - - def get_entry_point(self): - """ - Returns the slug of the step which the workflow should begin on. - - This method takes into account both already-available data and errors - within the steps. - """ - # If we have a valid specified entry point, use it. - if self.entry_point: - if self.get_step(self.entry_point): - return self.entry_point - # Otherwise fall back to calculating the appropriate entry point. - for step in self.steps: - if step.has_errors: - return step.slug - try: - step._verify_contributions(self.context) - except exceptions.WorkflowError: - return step.slug - # If nothing else, just return the first step. - return self.steps[0].slug - - def _trigger_handlers(self, key): - responses = [] - handlers = [(step.slug, f) for step in self.steps - for f in step._handlers.get(key, [])] - for slug, handler in handlers: - responses.append((slug, handler(self.request, self.context))) - return responses - - @classmethod - def register(cls, step_class): - """ Registers a :class:`~horizon.workflows.Step` with the workflow. """ - if not inspect.isclass(step_class): - raise ValueError('Only classes may be registered.') - elif not issubclass(step_class, cls._registerable_class): - raise ValueError('Only %s classes or subclasses may be registered.' - % cls._registerable_class.__name__) - if step_class in cls._cls_registry: - return False - else: - cls._cls_registry.add(step_class) - return True - - @classmethod - def unregister(cls, step_class): - """ - Unregisters a :class:`~horizon.workflows.Step` from the workflow. - """ - try: - cls._cls_registry.remove(step_class) - except KeyError: - raise base.NotRegistered('%s is not registered' % cls) - return cls._unregister(step_class) - - def validate(self, context): - """ - Hook for custom context data validation. Should return a boolean - value or raise :class:`~horizon.exceptions.WorkflowValidationError`. - """ - return True - - def is_valid(self): - """ - Verified that all required data is present in the context and - calls the ``validate`` method to allow for finer-grained checks - on the context data. - """ - missing = self.depends_on - set(self.context.keys()) - if missing: - raise exceptions.WorkflowValidationError( - "Unable to complete the workflow. The values %s are " - "required but not present." % ", ".join(missing)) - - # Validate each step. Cycle through all of them to catch all errors - # in one pass before returning. - steps_valid = True - for step in self.steps: - if not step.action.is_valid(): - steps_valid = False - step.has_errors = True - if not steps_valid: - return steps_valid - return self.validate(self.context) - - def finalize(self): - """ - Finalizes a workflow by running through all the actions in order - and calling their ``handle`` methods. 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 - for step in self.steps: - try: - data = step.action.handle(self.request, self.context) - if data is True or data is None: - continue - elif data is False: - partial = True - else: - self.context = step.contribute(data or {}, self.context) - except: - partial = True - exceptions.handle(self.request) - if not self.handle(self.request, self.context): - partial = True - return not partial - - def handle(self, request, context): - """ - Handles any final processing for this workflow. Should return a boolean - value indicating success. - """ - return True - - def get_success_url(self): - """ - Returns a URL to redirect the user to upon completion. By default it - will attempt to parse a ``success_url`` attribute on the workflow, - which can take the form of a reversible URL pattern name, or a - standard HTTP URL. - """ - try: - return urlresolvers.reverse(self.success_url) - except urlresolvers.NoReverseMatch: - return self.success_url - - def format_status_message(self, message): - """ - Hook to allow customization of the message returned to the user - upon successful or unsuccessful completion of the workflow. - - By default it simply inserts the workflow's name into the message - string. - """ - if "%s" in message: - return message % self.name - else: - return message - - def render(self): - """ Renders the workflow. """ - workflow_template = template.loader.get_template(self.template_name) - extra_context = {"workflow": self} - if self.request.is_ajax(): - extra_context['modal'] = True - context = template.RequestContext(self.request, extra_context) - return workflow_template.render(context) - - def get_absolute_url(self): - """ Returns the canonical URL for this workflow. - - This is used for the POST action attribute on the form element - wrapping the workflow. - - For convenience it defaults to the value of - ``request.get_full_path()`` with any query string stripped off, - e.g. the path at which the workflow was requested. - """ - return self.request.get_full_path().partition('?')[0] - - def add_error_to_step(self, message, slug): - """ - Adds an error to the workflow's Step with the - specifed slug based on API issues. This is useful - when you wish for API errors to appear as errors on - the form rather than using the messages framework. - """ - step = self.get_step(slug) - if step: - step.add_error(message) |