= FastFormward = Previously named !CherryForms, but name change was requested as this module isn't a builtin, or otherwise CherryPy core related. '''Note:'''[[br]] This documentation is far from perfect. Browser through it at least once and take a look at the samples. If any good doc writer is availble: feel free to rearrange and rewrite :) == What is it? == FastFormward is a library for quickly developing simple forms. == Why for crying out loud?! == There are plenty other form generators around, and i inspected merely a few.. [[br]] None were clean and obvious enough to grasp in 5 minutes, and all needed an adaption for CherryPy. That's not what i was looking for, so i wrote {{{YetAnotherFormGeneratorAndValidator}}}. According to [http://groups-beta.google.com/group/cherrypy-users/browse_frm/thread/4354dfed3bb6c3c1/ this thread on the users-list] i wasn't the only one seeking a nice sollution. :) === When to use it? === - well. I'm currently developing busines intranet applications. So they have to be developed fast, should be easily adaptable, and the layout is most of the times not that interesting. . Right now, i'm learning to do ''Agile development'' (no flames about this please) and one of the things i want to use this library for is avoiding the reccuring development of data entry forms. === What kinda applications? === You probabely don't want to use this when building a Forum or such.. [[br]] Development was aimed at intranet business applications. Saving me time with the simple screen, to focus on the more important stuff. So mainly, it should be a time-saver ;) == So you think it's the best? == Nope.. i will not have that illusion. It's more like ''yet another sollution'', and as long as it suits me fine, i'm working with- and on it. [http://www.cherrypy.org/wiki/RemcoBoerma Remco] ----- == Pro's and cons? == Cons: * Very very alfa!! * At the moment, there aren't enough fields supported out-of-the-box * At the time of writing, the {{{Field}}} implementations are quite 'hackish' * The implementation so far, is more a proof of concept, than a use-for-production library. * You are looking at all the external documentation, the rest is inline * Right now, doesn't support opening multiple windows to the same url within the same session. Pro's: * It's highly pythonic * Built for cherrypy * Seperates HTML and Python code as much as possible: Your logic is in Python, the HTML is constructed, and CSS is used for layout. (Near) perfect seperation :) * Allows for easy integration with templating filters (like the charming XyaptuFilter :) ) * Highly adaptable * Allows a single {{{Field}}} to use multiple html form fields. See {{{CompoundFields}}}. * Develop forms '''fast''' == CompoundFields == {{{CompoundField}}}s consist of a single {{{Field}}} derived class, that handles multiple html form fields. Think of the following data structures that would allow for easy development of compound fields. But please do remember: converting your form to a {{{CompoundField}}} is only a good thing to do, when you will use that input structure quite often. . * Dates (day, month, year) * Users (login name, real name, email) * Authorisations (, note, choice) Hers is a very simple example of a {{{DateField}}} using 3 inputs, and uses the {{{datetime}}} module to validate and convert the entered data: {{{ #!python class DateField(CompoundField): def __init__(self,name, label=None): "Enter a name, a list of fields, and an optional label." self.day = IntField("day",'Day') self.month = IntField("month",'Month') self.year = IntField("year",'Year') super(DateField,self).__init__(name,[self.day, self.month, self.year],label) def validate(self,kwp): "validates the date, using time module" try: self.pyValue(kwp) except: return False return True def errorMsg(self,kwp): try: self.pyValue(kwp) except Exception, e: return str(e) return '' def pyValue(self,kwp): vals = super(DateField,self).pyValue(kwp) import datetime return datetime.date(**vals) # and now to use it: # in Root.__init__(self,...): self.d = forms.Form(name='birthday', onExit = onExit, fields = [forms.DateField('birthday','Please enter your birthday')], submit = forms.SubmitField('next','Next: ',value='Go!'), confirm = None) # see below for more detailsa and other samples }}} == Extendible, you say? == Yes, as the form code is built around a [http://en.wikipedia.org/wiki/Petri_net PetriNet], one can easily extend the system to introduce even more different behaviour. The code to do this was inspired by the lovely, and very simple workflow module from [http://www.ikaaro.org/itools/ Ikaaro's itools]. Well, and a note by Guido van Rossum (which i can't seem to find anywhere anymore) on doing workflow with a dictionary. But most of all, the library of supported fields is extendible. [[br]] Based on a {{{Field}}} object, many other other field types can be generated. The most basic are the {{{TextField}}} and the {{{IntField}}}. ----- == Okay, you got my attention. . explain == Well. Let's start with some {{{Form}}} and {{{Field}}} documentation : {{{ Help on class Form in module forms: class Form(__builtin__.object) | A form is a cherrypy page... | it has the following states: | X. ...nothingness... | A. New Entry | B. Users Enters Data | C. Validate Entered Data | D. Getting Confirmation From User | E. ReEntry Filled With Entered Values | F. ReEntry Filled With Entered Values and error message | G. Entry Completed | Transitions: (obvious activity between () ) | | X --> A : born (form is setup, run only once!!) | | A --> B : (html is brewn) show form on screen | | B --> C : users posts results | | C --> D : input is valid, but a user confirmation was set to be asked, | (brew confirmation html) | | C --> G : input is valid, but no user confirmation was set to be asked, | | C --> F : input is not valid | (brew html with data and error msgs) | | F --> B : user posts new results | | E --> B : user posts new results | | G --> X : (fire another (external) method to build new screen) | | The form entry should all be done using 1 url, and preferable no hidden | form inputs to differentiate the states.. Otherwise it would be to easy | to hack those values.. So keeping the state purely serverside (and there- | for also not using different urls for the different states) allows for | less hackable data entry. | | The concept used to solve this, is to change the check the session for | the state for this form, and switch to the right function accordingly. | | Methods defined here: | | A_onEnter(self, kwp) | yield entry HTML, kwp is bluntly passed onto the fields .build call. | | B_onEnter(self, kwp) | state B isn't a state where HTML is produced.. | it's entered just before the validation starts | in state C | Stores the KWP in the session! | | C_onEnter(self, kwp) | Let's move on | | D_onEnter(self, kwp) | same as A_onEnter, but now calls the .build's with 'readonly=True' and with a confirmation input. | | E_onEnter(self, kwp) | in coding, this is the same as A_onEnter, so it uses that. | | F_onEnter(self, kwp) | If there is an invalid value in kwp | the html with warning should be displayed, this is done i each | .build() issued.. | | G_onEnter(self, kwp) | Let's move on | | X_onEnter(self, kwp) | If we _enter_ this, it means we have reached valid and possibly confirmed input. | Therefore, the onExit method is called (and it's result returned!), with the kwp from A_onEnter... | | __call__(self, **kwp) | CP2 helper to map /foo/bar to /foo/bar/default | | __init__(self, name, onExit, fields, confirm, submit=None, invalidForm=None, encoding='application/x-www-form-urlencoded') | name, # form name | onExit, # onExit method, will be called with a dictionary | # where key,value = key of the fields, pyValue of the | # form fields. | fields, # list of fields (order, _is_ important) | confirm, # user should confirm after good entry, field type | # with the question | submit = None, # optional submit input type (can be a combined) | # will be left out in readonly data | invalidForm = None, # specify to overwrite self.invalidForm | encoding = 'application/x-www-form-urlencoded' # form encoding, | # to support files etc... | # see http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.3 | | confirmationOk(self, kwp) | Does the self.confirm field validate, and is bool(self.confirm.pyValue(kwp) ) true? | Cause if it is, the user has confirmed the entered data. | | default(self, **kwp) | CP2 method to produce HTML. Will determine the next state and call the onEnter function for that state. | | index(self, **kwp) | CP2 helper to map /foo/bar/ to /foo/bar/default | | invalidEntry(self, kwp) | | invalidForm(self, kwp) | InValid Form? kwp is the dict with fields | is called when all fields have valid input | but there needs to be constraints checking | to see if the logic between the fields is okay. | return False if the fields are OKAY, or return | an error msg to tell the user what's wrong. | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __dict__ = | dictionary for instance variables (if defined) | | __weakref__ = | list of weak references to the object (if defined) ------------------- Help on class Field in module forms: class Field(__builtin__.object) | Methods defined here: | | __init__(self, type, name, label=None, html=None, id=None, tip=None, style=None, cssclass=None, helpmsg=None, validate=None, value=None, **extraAttrs) | Base Field type. Use derivatives.. | type, # type of input ('compound' for custom compound inputs) | name, # name of the input (or a list of names) | label=None, # the label to precede the input | html=None, # direct html entry | id=None, # the dom ID | tip=None, # tooltip, rendered probabely as title attribute | style=None, # any direct style input | cssclass=None, # the CSS class | helpmsg=None, # any default help msg if input fails. | validate=None, # specific validation method (made an instancemethod using module new) | value=None, # value if already known, can be anything, to support compound fields | **extraAttrs): # support for unknown tags | | build(self, form, readonly=False) | Called to build the html for this field, for the specified form. | Should return a string. | If readonly: the html should be rendered as readonly on the client side | | errorMsg(self, form) | Override this: so you can write specific error msgs based on the data in form. | | getHtml(self, form={}) | Property get method for html. the form parameter is the dictionary with form parameters | | pyValue(self, form) | Override this: return the python variant of this input based on the form dictionary. | | validate(self, form) | Returns either (True, value) or an (False,error message ) with the given form. | This means, that the Field will have to do it's own checking if there is a value | for it in the form. | Design decision: why not pass the corresponding value? | - well, that is an option. But when using multiple values using a compund | field, this can best be left to the specific Field.. | | ---------------------------------------------------------------------- | Properties defined here: | | html | Generated or static html for input.. | | = getHtml(self, form={}) | Property get method for html. the form parameter is the dictionary with form parameters | | = _setHtml(self, html) | | = _delHtml(self) | | ---------------------------------------------------------------------- | Data and other attributes defined here: | | __dict__ = | dictionary for instance variables (if defined) | | __weakref__ = | list of weak references to the object (if defined) }}} Now, i hope this is obvious enough, but a little test run can do no harm. [[br]] Let's pretend we run the following code... (as part of it is pure code, and another part is pure output...) {{{ #!python # let's say we have in integer field # with a input name of 'int', a label 'Enter an integer:' and a helpmsg if the # input doesn't validate... >>> i = forms.IntField('int','Enter an integer:',helpmsg='Use only digits...') # Now, let's define a function we want called when the entry is over. # (this could be a redirect to another form, or do something else. # be creative! :) (With some reassinging in the root structure, it is # not hard to write a flowable application. 1 url: complete application. # porting to the commandline would be easy. - right Carlos?! ) >>> def onExit(kwp): >>> print '--- got the keyword parameters. redirecting to other page.. ' >>> print 'kwp:',`kwp` >>> print 'done' >>> # we have ourselves an onExit function, and an input field. # Let's create a form: # * name : form name (used for storing the entry state in the session) # * onExit : is obviously the callback when entry is valid and complete # * fields : a list of fields (put in html in that order - use CSS for # extra layout requirements. # * confirm : None: there is no confirmation function - we'll get back at # this later. >>> f = forms.Form(name='formpje', onExit = onExit, fields = [i], confirm = None) # Let's pretend we're a browser request: >>> print "".join(f.default()) # we would get something like this: # [with extra linefeeds for readability - the result is linefeedless] # >> transition X -> A
# next, we pretend to enter invalid data: >>> print "".join(f.default(int='WRONG')) # >> transition A -> B # >> transition B -> C # >> transition C -> F
# next we enter valid data: >>> f.default(int='123') # >> transition F -> B # >> transition B -> C # >> transition C -> G # >> transition G -> X got the keyword parameters. redirecting to other page.. kwp: {'int': 123} done # that is to be expected, as we didn't provide a confirm field # which requests a validation of the entered data # the data should be offered readonly.. # so the onExit routine is called, with the kwp being set to the **kwp # default retrieves - and we can assume it's valid data... }}} == More samples == Here is a bit more of a smample. I've left out the output for clarity, get the module and use it if you'd like to see what this produces.. both root.f, root.g and root.h are forms. [[br]] Also note, that this is the entire {{{Root}}} class, and no external HTML or so is used... {{{ #!python class Root: def __init__(self): def onExit(kwp): yield 'terminated with kwp: ',`kwp` import forms # first form: simply enter an integer # [mind you, this will also work without a submit button, # so the submit button will leave from the output] self.f = forms.Form(name='formpje', onExit = onExit, fields = [forms.IntField('intje','geef intje:')], confirm = None) # same as self.f # but now with a cssclass for the input, and a confirmation request # after valid data has been read. self.g = forms.Form(name='formpje_with confirmation', onExit = onExit, fields = [forms.IntField('intje','geef intje:',cssclass="veldje")], confirm = forms.SubmitField('confirmed','Confirmed?', value='Confirmed')) # this will produce 3 integer fields. # so: # * the entry of each field must be valid # there is a veldConfirm to request confirmation # * the form uses confirmation after valid data # checkMultiply is used # * we add logic to test if the input values for this # form are correct. So we can test a bit more, if the input # relates to certain 'form'-logic constraints.. this is # typically where you would want to validate the entry # with databases etc. veldA = forms.IntField('A','A:') veldB = forms.IntField('B','B:') veldMul = forms.IntField('mult','multiplied') veldConfirm = forms.SubmitField('confirmed','Confirmed?', value='Confirmed') submit = forms.SubmitField(value='Submit') # this will check if the fields A * B == C # if not, form validation will fail (though every field validation might # have returned succesfully) and the error msg returned will be displayed # on screen. (not in any div or so, you should do that yourself..) # def checkmultiply(kwp): if veldA.pyValue(kwp) * veldB.pyValue(kwp) != veldMul.pyValue(kwp): return "A * B should be equal to C" ## here comes the form, available under /h/ self.h = forms.Form(name='formpje_with confirmation', onExit = onExit, fields = [veldA,veldB,veldMul], invalidForm = checkmultiply, submit = submit, confirm = veldConfirm) # now, ain't that easy? }}} == Haven't had enough? == Reach me at the CherryPyIrcChannel, or use one of the CherryPyMailingLists. == To do == * Remove extra states that aren't useful. (any state that isn't a join, or doesn't produce output is redundant) * more dox :) * more field types == History == See the latest attachment...