Source code for yota.nodes

from yota.exceptions import InvalidContextException
import copy


class Node(object):
    """ Nodes are holders of context for rendering and displaying validating
    for a portion of your :class:`Form`. This default base Node is designed to
    provide a template along with specific context information to a templating
    engine such as Jinja2. For validation a Node acts as an information source
    or an error sink. Essentially Nodes can be used to source data for use in a
    :class:`Check`, and they can then be delivered some sort of validation
    error via a the internal :attr:`errors` attribute.

    .. note:: By default all keyword attributes passed to a Node's init
        function are passed onto the rendering context. To override this,
        use the
        :attr:`Node._ignores` attribute.

    :param _attr_name: This is how the Node is identified in the Form. If
        populated automatically if the Node is defined in an a Form class
        definition, however if the Node is added dynamically it will need to be
        defined before adding it to the Form.
    :type _attr_name: string

    :param _ignores: A List of attribute names to explicity not include in the
        rendering context. Mostly a niceity for keeping the rendering context
        clutter free.
    :type _ignores: list

    :param _requires: A List of attributes that will be required at
        render time. An exception will be thrown if these attributes are not
        present.  Useful for things like lists that require certain data to
        render properly.
    :type _requires: list

    :param template: String name of the template to be parsed upon
        rendering.  This is passed into the `Form._renderer` so it needs to
        be whatever that is designed to accept. Jinja2 is looking for a
        filename like 'node' that occurs in it's search path.
    :type template: string

    :param validators: An optional attribute that specifies a :class:`Check`
        object, or list of Check objects to be associated with the Node. This is
        automatically at render time.
    :type validator: callable

    :param _null_val: When form submission data is passed in for validation and
        the :meth:`Node.resolve_data` method cannot identify anything, the data
        attribute will be set to this value. Defaults to "".

    The default Node init method accepts any keyword arguments and adds them to
    the Node's rendering context. In addition any class attributes may be added
    to custom Nodes and these attributes will be copied at instantiation time
    and passed into the rendering context.
    """

    _create_counter = 0
    """ Allows tracking the order of Node creation """
    _ignores = ['template', 'validator']
    _requires = []
    _attr_name = None
    _null_val = ""

    piecewise_trigger = 'blur'
    template = None
    validators = []
    label = True
    errors = []
    data = ''

    def __init__(self, **kwargs):
        # A bit of a hack to copy all our class attributes
        for class_attr in dir(self):
            if class_attr in kwargs:
                continue
            # We want to copy all the nodes as well as the list, this is a
            # succinct way to do it
            # Private attributes are internal stuff..
            if not class_attr.startswith('__'):
                # don't try to copy functions, it doesn't go well
                att = getattr(self, class_attr)
                if not callable(att):
                    setattr(self, class_attr, copy.copy(att))
        self.__dict__.update(kwargs)

        # Allows the parent form to keep track of attribute order
        self._create_counter = Node._create_counter
        Node._create_counter += 1

    def add_error(self, error):
        """ This method serves mostly as a wrapper alowing for different error
        ordering semantics, or possibly error post-processing. Errors from
        validation methods should be added in this way allowing them to be
        caught. More information about what gets passed in in the
        :doc:`Validators` section. """
        self.errors.append(error)

    def json_identifiers(self):
        """ Allows passing arbitrary identification information to your JSON
        error rendering callback. For instance, a common use case is the display
        an error message in a pre-defined div with a specific id. Well you may
        perhaps pass in an 'error_div_id' attribute to the JSON callback to use
        when trying to render this error. The default for Yota builtin nodes is
        to pass 'error_id' indicating the id of the error container in addition
        to a list containing all input elements in the Node's ids. """
        return {'error_id': self.id + '_error', 'elements': [self.id, ]}

    def set_identifiers(self, parent_name):
        """ This function gets called by the parent `Form` when it is
        initialized or inserted. It is designed to set various unique
        identifiers. By default it generates an id for the Node that is
        {parent_name}_{_attr_id}, a title for the Node that is the _attr_name
        capitalized, and a name for the element that is just the _attr_name.
        All of these attributes are then passed onto the rendering context of
        the Node by default. By default all of these attributes will yield to
        attributes passed into the __init__ method.

        :param parent_name: The name of the parent form. Useful in ensuring
            unique identifiers on your element names.
        :type parent_name: string
        """

        # Set some good defaults based on attribute name and parent name,
        # but always allow the user to override the values at the init level
        if not hasattr(self, 'id'):
            self.id = "{0}_{1}".format(parent_name, self._attr_name)
        if not hasattr(self, 'name'):
            self.name = self._attr_name
        if not hasattr(self, 'title'):
            self.title = self._attr_name.capitalize().replace('_', ' ')

    def resolve_data(self, data):
        """ This method links data from form submission back to Nodes. HTML
        form data is represented by a dictionary that is keyed by the 'name'
        attribute of the form element. Since most Nodes only render a single
        form element, and the default set_identifiers generates a single 'name'
        attribute for the Node then this function attempts to find data by
        linking the two together. However, if you were to change that semantic
        this would need to change. Look at the CheckGroupNode for a reference
        impplementation of this behaviour, or the Docs under "Custom Nodes".
        This method should operate by setting its own data attribute, as this
        is how Validators conventionally look for data.

        ... note:: This method will throw an exception at validation time if
            the data dictionary contains no key name, so it important to
            override this function to a NoOp if your Node generates no data.
            NonDataNode was created for this exact purpose.

        :param data: The dictionary of data that is passed to your validation
            method call.
        """
        try:
            self.data = data[self.name]
        except KeyError:
            self.data = self._null_val

    def get_context(self, g_context):
        """ Builds our rendering context for the Node at render time. By
        default all attributes of the Node are added to the global namespace
        and the global rendering context is passed in under the variable 'g'.
        This function is designed to be overridden for customization.  :param
        g_context: The global rendering context passed in from the rendering
        method.

        :param g_context: This is the global context passed in from the parent
            Form object. By default it's included under the 'g' key, similar to
            Flask's globals.
        """

        # Dat 2.6 compat, no dict comprehensions :(
        d = {}
        for key in dir(self):
            attr = getattr(self, key)
            if not key.startswith("_") and\
               key not in self._ignores and\
               not callable(attr):
                d[key] = attr

        # check to make sure all required attributes are present
        for r in self._requires:
            if r not in d:
                raise InvalidContextException(
                    "Missing required context value '{0}'".format(r))
        d['g'] = g_context
        return d

    def get_list_names(self):
        """ As the title suggests this needs to return an iterable of names. These
        should be names corresponding to form elements that the Node will
        generate. This list is uesed by piecewise validation to determine if a
        Node has been visisted based on a list of names that have been visited,
        bridging Nodes to elements. """
        return (self.name, )

    def __iter__(self):
        """ A simple way to make functions accept lists or single elements """
        yield self

    def __repr__(self):
        """ Make debugging and printing nodes a bit more readible """
        return "<{0} at {1}, _attr_name={2}>".format(__name__, id(self), self._attr_name)


class Blueprint(object):
    def __init__(self, source):
        for node in source._node_list:
            # Reassign attribute order to fit in line with the other attributes
            node._create_counter = Node._create_counter
            Node._create_counter += 1
        self._node_list = source._node_list
        self._event_lists = source._event_lists
        self._validation_list = source._validation_list


[docs]class BaseNode(Node): """ This base Node supplies the name of the base rendering template that is used for standard form elements. This base template provides error divs and the horizontal form layout for Bootstrap by default through the `horiz.html` base template. """ base = "horiz.html" css_class = '' css_style = ''
[docs]class NonDataNode(Node): """ A base to inherit from for Nodes that aren't designed to generate output, such as the SubmitNode or the LeaderNode. It must override resolve_data, otherwise the data will be set to :attr:`Node._null_val`. """ def resolve_data(self, data): pass
[docs]class ListNode(BaseNode): """ Node for providing a basic drop down list. Requires an attribute that is a list of tuples providing the key and value for the dropdown list items. .. note:: The first item of the tuple must be a string in order to match returned data properly and re-select the same list item when a validation error occurs. :attr items: Must be a list of tuples where the first element is the value of the second is the label. """ template = 'list' _requires = ['items']
[docs]class RadioNode(BaseNode): """ Node for providing a group of radio buttons. Requires buttons attribute. :attr buttons: Must be a list of tuples where the first element is the value of the second is the label. """ template = 'radio_group' _requires = ['buttons']
class CheckNode(BaseNode): """ Creates a simple checkbox for your form. """ template = 'checkbox' def resolve_data(self, data): if self.name in data: self.data = data[self.name] else: # Unchecked checkboxes don't submit any data so we'll set the # value to false if there is no post data self.data = False
[docs]class CheckGroupNode(BaseNode): """ Node for providing a group of checkboxes. Requires boxes attribute. Instead of defining an ID value explicitly the :class:`Node.set_identifiers` defines a prefix value to be prefixed to all id elements for checkboxes in the group. The output data is a list containing the names of the checkboxes that were checked. :attr boxes: Must be a list of tuples where the first element is the name, the second is the label. """ template = 'checkbox_group' _requires = ['boxes'] def resolve_data(self, data): # return a list of checked values since we have multiple names ret = [] for name, desc in self.boxes: try: if len(data[name]) > 0: ret.append(name) except KeyError: pass self.data = ret def json_identifiers(self): ids = [] for name, desc in self.boxes: ids.append(self.prefix + name) return {'error_id': self.id + "_error", 'elements': ids} def set_identifiers(self, parent_name): # defines a prefix to be used on all the different checkbox ids if not hasattr(self, 'prefix'): self.prefix = parent_name + "_" # defines a generic id to be used for generating things like error ids if not hasattr(self, 'id'): self.id = parent_name + "_" + self._attr_name if not hasattr(self, 'title'): self.title = self._attr_name.capitalize().replace('_', ' ')
[docs]class ButtonNode(BaseNode, NonDataNode): """ Creates a button in your form that submits no data. """ template = 'button' button_title = 'Click me!'
[docs]class EntryNode(BaseNode): """ Creates an input box for your form. """ template = 'entry'
[docs]class PasswordNode(BaseNode): """ Creates an input box for your form. """ template = 'password'
[docs]class FileNode(BaseNode): """ Creates an input box for your form. """ template = 'file' accepts = 'audio/*,video/*,image/*'
[docs]class TextareaNode(BaseNode): """ A node with a basic textarea template with defaults provided. :attr rows: The number of rows to make the textarea :attr columns: The number of columns to make the textarea """ template = 'textarea' rows = '5' columns = '10'
[docs]class SubmitNode(NonDataNode, BaseNode): """ Displays a submit button on the right side to align with Form elements """ template = 'submit' css_class = 'btn btn-primary'
[docs]class LeaderNode(NonDataNode): """ A Node that does few special things to setup and close the form. Intended for use in the start and end Nodes. """ form_class = "form-horizontal" action = '' def set_identifiers(self, parent_name): # set our start node's id to actually be the name of the form if not hasattr(self, 'id'): self.id = parent_name if not hasattr(self, 'name'): self.name = self._attr_name

Project Versions

Table Of Contents

This Page