Source code for yota.nodes

from yota.exceptions import InvalidContextException
import simplecaptcha.visual.tests
import pysistor
import copy


[docs]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 msgs = [] 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
[docs] def add_msg(self, *args, **kwargs): """ This method serves mostly as a wrapper alowing for different message ordering semantics, or possibly error post-processing. Messages from validation methods should be added in this way allowing them to be pre-processed if needed. More information about what gets passed in in the :doc:`Validators` section. """ # Allow them to pass a dictionary in position one, or define kwargs if len(args) > 0: self.msgs.append(args[0]) else: self.msgs.append(kwargs)
[docs] 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, ]}
[docs] 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('_', ' ')
[docs] def resolve_data(self, data): """ This method links data from input data into the Nodes. By default 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. :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
[docs] 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
[docs] 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 class Base(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 = '' class NonData(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 class List(Base): """ 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'] class Radio(Base): """ 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 Checkbox(Base): """ Creates a simple checkbox for your form. """ template = 'checkbox' def resolve_data(self, data): if self.name in data: self.data = True 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 class CheckboxGroup(Base): """ 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('_', ' ') class Button(Base, NonData): """ Creates a button in your form that submits no data. """ template = 'button' button_title = 'Click me!' class Captcha(Base): """ A node designed for basic captcha support. It expects to receive a captcha id through the global context and uses the id to reference a captcha image. """ template = 'captcha' placeholder = 'Are you human?' captcha_test = simplecaptcha.visual.tests.AngryGimpy def get_context(self, g_context): import pysistor import pickle import datetime # Perform the super context as normal context = Base.get_context(self, g_context) # Then generate a new Captcha Node from Pysistor and inject the # information test = self.captcha_test() pysistor.Pysistor.expire_all("captcha_", backend=self._parent_form.pysistor_backend, adapter=self._parent_form.pysistor_adapter) pysistor.Pysistor.store("captcha_{0}".format(test.id), pickle.dumps(test), expire=datetime.datetime.now() - datetime.timedelta(minutes=5), backend=self._parent_form.pysistor_backend, adapter=self._parent_form.pysistor_adapter) context.update({'captcha_id': test.id}) return context def resolve_data(self, data): try: self.captcha_id = data['__captcha_id__'] self.data = data[self.name] except KeyError: raise DataAccessException("Node {0} cannot find name {1} in " "submission data.".format(self._attr_name, self.name)) class Entry(Base): """ Creates an input box for your form. """ template = 'entry' class Password(Base): """ Creates an input box for your form. """ template = 'password' class File(Base): """ Creates an input box for your form. """ template = 'file' accepts = 'audio/*,video/*,image/*' class Textarea(Base): """ 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' class Submit(NonData, Base): """ Displays a submit button on the right side to align with Form elements """ template = 'submit' css_class = 'btn btn-primary' class Leader(NonData): """ 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