CherryPy Project Download

FastFormward: forms.py

Line 
1 """
2 Cherry Forms - FastFormward
3
4 History:
5 0,3  @ 2005/05/07:
6     Remco:
7     - Support for fieldsets
8     - support for compound fields (proof is there)
9     - DateField implementation (look at how simple it is!)
10     - Changed a line or two in inline docs and samples
11 0,2  @ 2005/05/07:
12     Remco:
13     - Added submit button as SubmitField.
14     - Better support for submit buttons: they will not be displayed
15       when validating. Beware, if your submit button is part of your data entry
16       you might want to take good care of this :)
17     - Added support for form logic error msg: invalidForm should return
18       and error msg if the fields are not filled correctly. replaces validateForm
19     - the onExit function will no longer be called with the form data, but with
20       a dictionary where the key is the form name, and the value the pyValue(kwp)
21       for that field, with the value of kwp being extracted from the session
22       to avoid the entering new values hack.
23           
24 0.1  @ 2005/05/07:
25     Remco:
26     - gone public, still alfa, see wiki:
27       http://www.cherrypy.org/wiki/CherryForms
28
29 <not public>
30     Remco: first attempt at solving the problem
31     as described on the wiki page
32
33
34 Copyright (c) 2004, CherryPy Team (team@cherrypy.org)
35 All rights reserved.
36
37 Redistribution and use in source and binary forms, with or without modification,
38 are permitted provided that the following conditions are met:
39
40     * Redistributions of source code must retain the above copyright notice,
41       this list of conditions and the following disclaimer.
42     * Redistributions in binary form must reproduce the above copyright notice,
43       this list of conditions and the following disclaimer in the documentation
44       and/or other materials provided with the distribution.
45     * Neither the name of the CherryPy Team nor the names of its contributors
46       may be used to endorse or promote products derived from this software
47       without specific prior written permission.
48
49 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
50 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
51 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
52 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
53 FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
54 DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
55 SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
56 CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
57 OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
58 OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
59 """
60
61
62 __version__ = "0.1"
63 __authors__ = ['Remco Boerma: remco.boerma@gmail.com']
64 from cherrypy import cpg
65
66
67 ######################################################################
68 ##
69 ## ToSession and FromSession
70 ## from http://www.cherrypy.org/wiki/FromAndToSession
71 ##
72 ######################################################################
73
74 def toSession(**kwp):
75     """Stores the keyword parameters in the cpg.request.sessionMap.
76     like:
77        toSession(bla=bla) will store the variable bla as 'bla'
78     @returns: nothing.   
79     """
80     for k,v in kwp.items():
81         cpg.request.sessionMap[k] = v
82
83 def fromSession(*names):
84     """retreives the names from the cpg.request.sessionMap.
85     like:
86        bla,pir = fromSession('bla','pir') will load bla and pir
87     @returns: the values for the keys named in 'names'
88     @raises: nothing, None as value for a key that can't be found
89     """
90     values = []
91     for name in names:
92         try:
93             values.append(cpg.request.sessionMap.get(name,None))
94         except AttributeError:
95             values.append(None)
96     if len(values) ==1:
97             ## allow for easy element retreival
98             return values[0]
99     else:
100             return values
101
102
103
104 ######################################################################
105 ##
106 ## Fields
107 ##
108 ######################################################################
109
110 import new
111 class Field(object):
112     def __init__(self,
113                  type,          # type of input ('compound' for custom compound inputs)
114                  name,          # name of the input (or a list of names)
115                  label=None,    # the label to precede the input
116                  html=None,     # direct html entry
117                  id=None,       # the dom ID
118                  tip=None,      # tooltip, rendered probabely as title attribute
119                  style=None,    # any direct style input
120                  cssclass=None, # the CSS class
121                  helpmsg=None,  # any default help msg if input fails.
122                  validate=None, # specific validation method (made an instancemethod using module new)
123                  value=None,    # value if already known, can be anything, to support compound fields
124                  **extraAttrs): # support for unknown tags
125         """
126         Base Field type. Use derivatives..
127          type,          # type of input ('compound' for custom compound inputs)
128          name,          # name of the input (or a list of names)
129          label=None,    # the label to precede the input
130          html=None,     # direct html entry
131          id=None,       # the dom ID
132          tip=None,      # tooltip, rendered probabely as title attribute
133          style=None,    # any direct style input
134          cssclass=None, # the CSS class
135          helpmsg=None,  # any default help msg if input fails.
136          validate=None, # specific validation method (made an instancemethod using module new)
137          value=None,    # value if already known, can be anything, to support compound fields
138          **extraAttrs): # support for unknown tags
139         """       
140         assert type != None, 'Every input should be of _some_ type...'
141         if type != 'submit':
142             assert name != None, 'Every input should have _some_ name...'
143         self.type = type
144         self.name = name
145         if label:
146             self.label = label
147         if html:
148             self.html = html
149         if id:
150             self.id = id
151         else:
152             if name:
153                 self.id = "input."+name
154         if tip:
155             self.tip = tip
156         if style:
157             self.style = style
158         if cssclass:
159             self.cssclass = cssclass
160         if helpmsg:
161             self.helpmsg = helpmsg
162         if validate:
163             self.validate = new.instancemethod(validate,self)
164         if value:
165             self.value = value
166         self.extraAttrs = extraAttrs
167
168     def validate(self,form):
169         """
170         This means, that the Field will have to do it's own checking if there is a value
171         for it in the form.
172         Design decision: why not pass the corresponding value?
173             - well, that is an option. But when using multiple values using a compund
174               field, this can best be left to the specific Field.. """
175         return True
176        
177
178     def errorMsg(self,form):
179         "Override this: so you can write specific error msgs based on the data in form."
180         return self.helpmsg
181
182     def build(self,form,readonly=False):
183         """Called to build the html for this field, for the specified form.
184         Should return a string.
185         If readonly: the html should be rendered as readonly on the client side"""
186         if hasattr(self,'_html'):
187             return self._html
188         if form == None:
189             raise ValueError, 'Form cannot be None if no ._html was given for %r' % self
190         def attr(name,varname):
191             if not hasattr(self,varname):
192                 return ''
193             else:
194                 # test if the value is None,
195                 # if so, it's not shown
196                 value = getattr(self,varname)
197                 if value == None:
198                     return ''
199                 else:
200                     return '%s="%s"' % (name,value)
201         result = []
202         if hasattr(self,'label'):
203             result.append('<label %s >%s</label>' % (attr('for','id'), self.label))
204         result.append("<input %s %s %s %s %s %s " % (attr('type','type'),
205                                         attr('name','name'),
206                                         attr('id','id'),
207                                         attr('class','cssclass'),
208                                         attr('title','tip'),
209                                         attr('style','style')))
210         if form.keys() and not self.validate(form):
211             # if the input is invalid
212             # add the erroreous attribute
213             # so CSS can make it show a bit different
214             result.append(' erroreous ')
215             # an error msg could be put in the value...
216             result.append('value="%s:%s"' % (self.errorMsg(form),form.get(self.name,'')))
217         else:
218             result.append('value="%s"' % (form.get(self.name,'')))
219         # add extra key="value" pairs
220         result.extend(['%s="%s" ' % (pair) for pair in self.extraAttrs.items() if pair[1] != None])
221         # add extra key fields (like multiple etc.)
222         result.extend(['%s ' % key for key, value in self.extraAttrs.items() if value == None])
223         if readonly:
224             result.append(' readonly ')
225         result.append('>')
226         return "".join(result)
227            
228     def getHtml(self,form={}):
229         "Property get method for html. the form parameter is the dictionary with form parameters"
230         return self.build(form)
231
232     def _setHtml(self,html):
233         self._html = html
234
235     def _delHtml(self):
236         if hasattr(self,'_html'):
237             del self._html
238
239     html = property(getHtml,_setHtml,_delHtml,doc="Generated or static html for input..")
240     def pyValue(self,form):
241         "Override this: return the python variant of this input based on the form dictionary."
242         return None
243
244
245 class SubmitField(Field):
246     def __init__(self,name=None,label=None,value='Submit',**kwp):
247         "Create an integer entry"
248         super(SubmitField,self).__init__('submit',name,label,value=value,**kwp)
249
250     def validate(self,form):
251         return True
252     def build(self,kwp,readonly=False):
253         html = super(SubmitField,self).build(kwp,readonly)
254         # replace the value attribute, as it needs to be filled. .
255         # but as the self.name is not a key in the kwp, the Field.build
256         # method will not fill the value..
257         return html.replace('value=""','value="%s"' % self.value)
258
259     def pyValue(self,form):
260         "returns true if this button was used.."
261         keyAvailable = form.has_key(self.name)
262         if not keyAvailable:
263             return False
264         if getattr(self,'value',None):
265             return form[self.name] == self.value
266         else:
267             return keyAvailable
268
269 class IntField(Field):
270     def __init__(self,name,label,**kwp):
271         "Create an integer entry"
272         if 'helpmsg' not in kwp:
273             kwp['helpmsg'] = 'Enter digits only'
274         super(IntField,self).__init__('text',name,label,**kwp)
275
276     def validate(self,form):
277         value = form.get(self.name,'').strip()
278         return bool(len(value) and value.isdigit())
279
280     def pyValue(self,form):
281         if self.validate(form):
282             return int(form[self.name])
283         else:
284             return None
285
286 class SelectField(Field):
287     # UNTESTED, in progress
288     def __init__(self,name,label,multiple=False,tip=None,style=None,cssclass=None, values = None, **extraAttrs):
289         "Create a select (drop) list field. Values is a list of (value,label) pairs"
290         self.multiple = multiple
291         if multiple:
292             extraAttrs['multiple']=None
293         super(Select,self).__init__('text',name,label,
294                                       tip,style,cssclass
295                                       **extraAttrs
296                                       )
297         self.options = value
298
299     def build(self,form,readonly=False):
300         # get the value from there
301         val = super(SelectField,self).build(form,readonly)
302         val.replace('<input','<select') # make it a select
303         # add options
304         val += "".join(['<option label="%s">%s</option>' % (label,value) for value, label in self.options])
305         # add closing /select
306         val +='</select>'
307        
308
309
310 class RadioField(Field):
311     def __init__(self,name,label,value='',**extraAttrs):
312         "Create a field. "
313         super(RadioField,self).__init__('radio',name,label,
314                                       **extraAttrs)
315         self.value = value
316
317     def build(self,form,readonly=False):
318         # get the value from there
319         val = super(RadioField,self).build({},readonly)
320         if form.get(self.name,None)==self.value:
321             checked = 'CHECKED'
322         else:
323             checked = ''
324         return val.replace('value=""','value="%s" %s' % (self.value, checked))
325
326     def pyValue(self,form):
327         return form.get(self.name,None)
328
329
330 class CheckField(Field):
331     def __init__(self,name,label,value='',**extraAttrs):
332         "Create a check field. "
333         super(CheckField,self).__init__('checkbox',name,label,
334                                       **extraAttrs)
335         self.value = value
336
337     def build(self,form,readonly=False):
338         # get the value from there
339         val = super(CheckField,self).build({},readonly)
340         if form.get(self.name,None)==self.value:
341             checked = 'CHECKED'
342         else:
343             checked = ''
344         return val.replace('value=""','value="%s" %s' % (self.value, checked))
345
346     def pyValue(self,form):
347         vals = form.get(self.name,None)
348         if type(vals) == type([]):
349             return self.value in vals
350         if type(vals) == type(""):
351             return vals == self.value
352         else:
353             return False
354
355 ######################################################################
356 ##
357 ## FieldSet
358 ##
359 ######################################################################
360
361 class FieldSet(Field):
362     """Although a fieldset is not the same as a field,
363     it's an easy grouping of functions. Easiest to use
364     would be if you could use a fieldset as a field.
365     Fieldset should be quite easily converted to a comound field
366     structure though.
367     """
368     def __init__(self,name,fields, label=None):
369         "Enter a name, a list of fields, and an optional label."
370         # for a compound field, you should initialise the
371         # Field.__init__of course..
372         self.name = name
373         self.fields = fields
374         self.label = label
375
376     def validate(self,form):
377         for field in self.fields:
378             if not field.validate(form):
379                 return False
380         return True
381
382     def build(self,kwp,readonly=False):
383         yield '<fieldset>'
384         if self.label:
385             yield '<legend>%s</legend>' % self.label
386         if kwp and not self.validate(kwp):
387             yield self.errorMsg(kwp)
388         for field in self.fields:
389             yield field.build(kwp,readonly)
390         yield '</fieldset>'
391
392     def pyValue(self,form):
393         "returns a dictionary, like given to a form.onExit method..."
394         return dict([(field.name[len(self.name)+1:], # strip the compundname. part
395                       field.pyValue(form))
396                      for field in self.fields])
397
398     def errorMsg(self,form):
399         # a fieldset can't have an error.. fields will have that..
400         return ''
401
402 ######################################################################
403 ##
404 ## CompoundField Base
405 ##
406 ######################################################################
407 class CompoundField(FieldSet):
408     """Although a fieldset is not the same as a field,
409     it's an easy grouping of functions. Easiest to use
410     would be if you could use a fieldset as a field.
411     Fieldset should be quite easily converted to a comound field
412     structure though.
413     """
414     def __init__(self,name,fields, label=None):
415         "Enter a name, a list of fields, and an optional label."
416         super(CompoundField,self).__init__(name,fields,label)
417         # for a compound field, you should initialise the
418         # Field.__init__of course..
419         self.name = name
420         self.fields = fields
421         self.label = label
422         # make the fields children of this compund
423         for field in fields:
424             field.name= name+'.'+field.name
425             if hasattr(field,'id'):
426                 field.id= name+'.'+field.id
427             else:
428                 field.id = 'input'+name+'.'+field.name
429
430 class DateField(CompoundField):
431     def __init__(self,name, label=None):
432         "Enter a name, a list of fields, and an optional label."
433         self.day = IntField("day",'Day')
434         self.month = IntField("month",'Month')
435         self.year = IntField("year",'Year')
436         super(DateField,self).__init__(name,[self.day, self.month, self.year],label)
437
438     def validate(self,kwp):
439         "validates the date, using time module"
440         try:
441             self.pyValue(kwp)
442         except:
443             return False
444         return True
445
446     def errorMsg(self,kwp):
447         try:
448             self.pyValue(kwp)
449         except Exception, e:
450             return str(e)
451         return ''
452    
453     def pyValue(self,kwp):
454         vals = super(DateField,self).pyValue(kwp)
455         import datetime
456         return datetime.date(**vals)
457        
458        
459 ######################################################################
460 ##
461 ## Form Base class
462 ##
463 ######################################################################
464
465 class Form(object):
466     """A form is a cherrypy page...
467     it has the following states:
468         X. ...nothingness...
469         A. New Entry
470         B. Users Enters Data
471         C. Validate Entered Data
472         D. Getting Confirmation From User
473         E. ReEntry Filled With Entered Values
474         F. ReEntry Filled With Entered Values and error message
475         G. Entry Completed
476     Transitions: (obvious activity between () )
477
478         X --> A : born (form is setup, run only once!!)
479
480         A --> B : (html is brewn) show form on screen
481
482         B --> C : users posts results
483
484         C --> D : input is valid, but a user confirmation was set to be asked,
485                   (brew confirmation html)
486
487         C --> G : input is valid, but no user confirmation was set to be asked,
488
489         C --> F : input is not valid
490                   (brew html with data and error msgs)
491
492         F --> B : user posts new results
493
494         E --> B : user posts new results
495
496         G --> X : (fire another (external) method to build new screen)
497
498     The form entry should all be done using 1 url, and preferable no hidden
499     form inputs to differentiate the states.. Otherwise it would be to easy
500     to hack those values.. So keeping the state purely serverside (and there-
501     for also not using different urls for the different states) allows for
502     less hackable data entry.
503
504     The concept used to solve this, is to change the check the session for
505     the state for this form, and switch to the right function accordingly.
506     """
507
508     def __init__(self,
509                  name,    # form name
510                  onExit,  # onExit method, will be called with a dictionary
511                           # where key,value = key of the fields, pyValue of the
512                           # form fields.
513                  fields,  # list of fields (order, _is_ important)
514                  confirm, # user should confirm after good entry, field type
515                           # with the question
516                  submit = None, # optional submit input type (can be a combined)
517                           # will be left out in readonly data
518                  invalidForm = None, # specify to overwrite self.invalidForm
519                  encoding = 'application/x-www-form-urlencoded' # form encoding,
520                           # to support files etc...
521                           # see http://www.w3.org/TR/REC-html40/interact/forms.html#h-17.3
522                  ):
523         """
524          name,    # form name
525          onExit,  # onExit method, will be called with a dictionary
526                   # where key,value = key of the fields, pyValue of the
527                   # form fields.
528          fields,  # list of fields (order, _is_ important)
529          confirm, # user should confirm after good entry, field type
530                   # with the question
531          submit = None, # optional submit input type (can be a combined)
532      &