forked from ish/formish
/
forms.py
1037 lines (821 loc) · 34 KB
/
forms.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
The form module contains the main form, field, group and sequence classes
"""
import re
from peak.util.proxies import ObjectWrapper
from webob import UnicodeMultiDict
import schemaish, validatish
from formish import util
from dottedish import dotted, unflatten, set
from formish import validation
from formish import widgets
from formish.renderer import _default_renderer
NOARG = object()
def container_factory(parent_key, item_key):
if item_key.isdigit():
return []
return {}
def is_int(v):
""" raise error if not """
try:
int(v)
return True
except ValueError:
return False
class Action(object):
"""
An action that that can added to a form.
:arg name: an valid html id used to lookup an action
:arg value: The 'value' of the submit button and hence the text that people see
:arg callback: A callable with the signature (request, form, *args)
"""
def __init__(self, name=None, value=None, callback=None):
if name and not util.valid_identifier(name):
raise validation.FormError('Invalid action name %r.'% name)
self.callback = callback
self.name = name
self.value = value
def _cssname(self):
""" Returns a hyphenated identifier using the form name and field name """
if self.form.name:
return '%s-%s'% (self.form.name, '-'.join(self.name.split('.')))
return '%s'% ('-'.join(self.name.split('.')))
def _classes(self):
""" Works out a list of classes that should be applied to the field """
classes = [
'field',
re.sub('[0-9\*]+','n',_cssname(self)),
'type-%s'%self.attr.type.lower(),
]
classes.append('widget-%s'%self.widget.type.lower())
if self.required:
classes.append('required')
if self.widget.css_class is not None:
classes.append(self.widget.css_class)
if str(self.error):
classes.append('error')
if getattr(self,'contains_error',None):
classes.append('contains-error')
return ' '.join(classes)
def starify(name):
"""
Replace any ints in a dotted key with stars. Used when applying defaults and widgets to fields
"""
newname = []
for key in name.split('.'):
if is_int(key):
newname.append('*')
else:
newname.append(key)
name = '.'.join(newname)
return name
def fall_back_renderer(renderer, name, widget, vars):
"""
Tries to find template in widget directly then tries in top level directory
This allows a field level widget override it's container by including the
changed version in the widgets directory with the same name
"""
import mako
try:
return renderer('/formish/widgets/%s/%s.html'%(widget,name), vars)
except mako.exceptions.TopLevelLookupException, e:
return renderer('/formish/%s.html'%(name), vars)
class TemplatedString(object):
"""
A callable, teplated string
"""
def __init__(self, obj, attr_name, val):
self.obj = obj
self.attr_name = attr_name
self.val = val
def __str__(self):
if not self.val:
return ''
return unicode(self.val)
def __repr__(self):
if not self.val:
return ''
return unicode(self.val)
def __nonzero__(self):
if self.val:
return True
else:
return False
def __call__(self):
widget_type, widget = self.obj.widget.template.split('.')
renderer = self.obj.form.renderer
name = '%s/%s'%(widget_type,self.attr_name)
vars = {'field':self.obj}
return fall_back_renderer(renderer, name, widget, vars)
class Field(object):
"""
A wrapper for a schema field type that includes form information.
The Schema Type Atribute does not have any bindings to the form library, it can be
used on it's own. We bind the Schema Attribute to a Field in order to include form
related information.
:method __call__: returns a serialisation for this field using the form's renderer - read only
"""
type = 'field'
def __init__(self, name, attr, form):
"""
:arg name: Name for the field
:arg attr: Schema attr to bind to the field
:type attr: schemaish.attr.*
:param form: The form the field belongs to.
:type form: formish.Form instance.
"""
self.name = name
self.nodename = name.split('.')[-1]
self.attr = attr
self.form = form
def __repr__(self):
return 'formish.Field(name=%r, attr=%r)'% (self.name, self.attr)
@property
def title(self):
""" The Field schema's title - derived from name if not specified """
try:
return self.form.get_item_data(self.name,'title')
except KeyError:
if self.attr.title is not None:
return self.attr.title
else:
return util.title_from_name(self.name.split('.')[-1])
@property
def description(self):
""" The Field schema's description """
try:
val = self.form.get_item_data(self.name,'description')
except KeyError:
val = self.attr.description
return TemplatedString(self, 'description', val)
@property
def cssname(self):
""" cssname identifier for the field """
return _cssname(self)
@property
def classes(self):
""" Works out a list of classes that should be applied to the field """
return _classes(self)
@property
def value(self):
"""Convert the request_data to a value object for the form or None."""
if '*' in self.name:
return self.widget.to_request_data(self, self.defaults)
return self.form.request_data.get(self.name, None)
@property
def required(self):
""" Does this field have a Not Empty validator of some sort """
return validatish.validation_includes(self.attr.validator, validatish.Required)
@property
def defaults(self):
"""Get the defaults from the form."""
if '*' not in self.name:
defaults = self.form.defaults.get(self.name, None)
else:
defaults = self.form.get_item_data(self.name, 'default', None)
return defaults
@property
def error(self):
""" Lazily get the error from the form.errors when needed """
error = self.form.errors.get(self.name, None)
if error is not None:
val = str(error)
else:
val = ''
return TemplatedString(self, 'error', val)
def _get_errors(self):
""" Lazily get the error from the form.errors when needed """
return self.form.errors.get(self.name, None)
def _set_errors(self, v):
self.form.errors[self.name] = v
errors = property(_get_errors, _set_errors)
@property
def widget(self):
""" return the fields widget bound with extra params. """
# Loop on the name to work out if any '*' widgets are used
try:
widget_type = self.form.get_item_data(starify(self.name),'widget')
except KeyError:
widget_type = widgets.Input()
self.form.set_item_data(starify(self.name),'widget',widget_type)
return BoundWidget(widget_type, self)
def __call__(self):
""" returns a serialisation for this field using the form's renderer """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = 'field/main'
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def label(self):
widget_type, widget = self.widget.template.split('.')
""" returns the templated title """
renderer = self.form.renderer
name = 'field/label'
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def inputs(self):
""" returns the templated widget """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = 'field/inputs'
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
class CollectionFieldsWrapper(ObjectWrapper):
"""
Allow fields attr of a form to be accessed (as a generator) but also callable
"""
collection = None
def __init__(self, collection):
ObjectWrapper.__init__(self, iter(collection.collection_fields()))
self.collection = collection
def __call__(self):
widget_type, widget = self.collection.widget.template.split('.')
renderer = self.collection.form.renderer
name = '%s/fields'%widget_type
vars = {'field':self.collection}
return fall_back_renderer(renderer, name, widget, vars)
class Collection(object):
"""
A wrapper for a schema group type that includes form information.
The Schema structure does not have any bindings to the form library, it can
be used on it's own. We bind the schema Structure Attribute to a Group
which includes form information.
"""
type = None
def __init__(self, name, attr, form):
"""
:arg name: Name for the Collection
:arg attr: Schema attr to bind to the field
:type attr: schemaish.attr.*
:param form: The form the field belongs to.
:type form: formish.Form instance.
"""
self.name = name
if name is not None:
self.nodename = name.split('.')[-1]
else:
self.nodename = ''
self.attr = attr
self.form = form
self._fields = {}
# Construct a title
self.title = self.attr.title
if self.title is None and name is not None:
self.title = util.title_from_name(self.name.split('.')[-1])
@property
def template_type(self):
""" Returns the template type to use for this item """
if self.attr.type == 'Structure':
name = 'structure'
elif self.attr.type == 'Sequence' and self.widget.type == 'SequenceDefault':
name = 'sequence'
else:
name = 'field'
return name
@property
def description(self):
""" Returns the schema's description """
val = self.attr.description
return TemplatedString(self,'description',val)
@property
def cssname(self):
""" Works out a list of classes that can be applied to the field """
return _cssname(self)
@property
def classes(self):
"""
Works out a list of classes that can be applied to the field """
return _classes(self)
@property
def value(self):
"""Convert the request_data to a value object for the form or None."""
return self.form.request_data.get(self.name, [''])
@property
def required(self):
""" Does this field have a Not Empty validator of some sort """
return validatish.validation_includes(self.attr.validator, validatish.Required)
@property
def defaults(self):
"""Get the defaults from the form."""
defaults = self.form.defaults.get(self.name, None)
return defaults
@property
def error(self):
""" Lazily get the error from the form.errors when needed """
val = self.form.errors.get(self.name, None)
return TemplatedString(self, 'error', val)
def _get_errors(self):
""" Lazily get the error from the form.errors when needed """
return self.form.errors.get(self.name, None)
def _set_errors(self, v):
self.form.errors[self.name] = v
errors = property(_get_errors, _set_errors)
@property
def contains_error(self):
""" Check to see if any child elements have errors """
for k in self.form.errors.keys():
if k.startswith(self.name):
return True
return False
@property
def widget(self):
""" return the fields widget bound with extra params. """
try:
widget_type = BoundWidget(self.form.get_item_data(starify(self.name),'widget'),self)
except KeyError:
if self.type == 'group':
widget_type = BoundWidget(widgets.StructureDefault(),self)
else:
widget_type = BoundWidget(widgets.SequenceDefault(),self)
self.form.set_item_data(starify(self.name),'widget',widget_type)
return widget_type
def get_field(self, segments):
""" recursively get dotted field names """
for field in self.fields:
if field.name.split('.')[-1] == segments[0]:
if isinstance(field, Field):
return field
else:
return field.get_field(segments[1:])
def __getitem__(self, key):
return FormAccessor(self.form, '%s.%s'% (self.name, key))
@property
def attrs(self):
""" The schemaish attrs below this collection """
return self.attr.attrs
def collection_fields(self):
for attr in self.attrs:
yield self.bind(attr[0], attr[1])
@property
def fields(self):
"""
Iterate through the fields, lazily bind the schema to the fields
before returning.
"""
return CollectionFieldsWrapper(self)
def bind(self, attr_name, attr):
"""
return cached bound schema as a field; Otherwise bind the attr to a
Group or Field as appropriate and store on the _fields cache
:param attr_name: Form Field/Group identifier
:type attr_name: Python identifier string
:param attr: Attribute to bind
:type attr: Schema attribute
"""
try:
return self._fields[attr_name]
except KeyError:
if self.name is None:
keyprefix = attr_name
else:
keyprefix = '%s.%s'% (self.name, attr_name)
if isinstance(attr, schemaish.Sequence):
bound_field = Sequence(keyprefix, attr, self.form)
elif isinstance(attr, schemaish.Structure):
bound_field = Group(keyprefix, attr, self.form)
else:
bound_field = Field(keyprefix, attr, self.form)
self._fields[attr_name] = bound_field
return bound_field
def __call__(self):
""" returns a serialisation for this field using the form's renderer """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = '%s/main'%widget_type
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def label(self):
""" returns the templated title """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = '%s/label'%widget_type
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def metadata(self):
""" returns the metadata """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = '%s/metadata'%widget_type
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def inputs(self):
""" returns the templated widget """
widget_type, widget = self.widget.template.split('.')
renderer = self.form.renderer
name = '%s/inputs'%widget_type
vars = {'field':self}
return fall_back_renderer(renderer, name, widget, vars)
def __repr__(self):
return 'formish.%s(name=%r, attr=%r)'% (self.type.title(), self.name, self.attr)
class Group(Collection):
"""
A group is a basic collection with a different template
"""
type = 'group'
template = 'structure'
class Sequence(Collection):
"""
A sequence is a collection with a variable number of fields depending on request data, data or min/max values
"""
type = 'sequence'
template = 'sequence'
def collection_fields(self):
"""
For sequences we check to see if the name is numeric. As names cannot be numeric normally, the first iteration loops
on a fields values and spits out a
"""
# Work out how many fields are in the sequence.
# XXX We can't use self.form.request_data here because to build the
# request_data we need to recurse throught the fields ... which calls
# Sequence.fields ... which tries to build the request data ... which
# calls Sequence.fields, etc, etc. Bang!
if not self.form._request_data:
if self.defaults is not None:
num_fields = len(self.defaults)
else:
num_fields = 0
num_nonempty_fields = 0
if self.widget is not None:
empty_checker = self.widget.empty_checker
if self.defaults is not None:
for n,d in enumerate(self.defaults):
if not empty_checker(d):
num_nonempty_fields=n+1
min_start_fields = None
min_empty_start_fields = None
if self.widget is not None:
min_start_fields = getattr(self.widget, 'min_start_fields', None)
min_empty_start_fields = getattr(self.widget, 'min_empty_start_fields', None)
if min_start_fields is not None and num_fields < min_start_fields:
num_fields = min_start_fields
if min_empty_start_fields is not None and (num_fields - num_nonempty_fields) < min_empty_start_fields:
num_fields = num_nonempty_fields + min_empty_start_fields
else:
num_fields = len(self.form._request_data.get(self.name, []))
for num in xrange(num_fields):
field = self.bind(num, self.attr.attr)
yield field
@property
def template(self):
return self.bind('*', self.attr.attr)
class BoundWidget(object):
"""
Because widget's need to be able to render themselves
"""
def __init__(self, widget, field):
self.__dict__['widget'] = widget
self.__dict__['field'] = field
def __getattr__(self, name):
return getattr(self.widget, name)
def __setattr__(self, name, value):
setattr(self.widget, name, value)
def __call__(self):
widget_type, widget = self.widget.template.split('.')
if self.widget.readonly == True:
widget_template = 'readonly'
else:
widget_template = 'widget'
return self.field.form.renderer('/formish/widgets/%s/%s.html'%(widget, widget_template), {'field':self.field})
def __repr__(self):
return 'BoundWidget(widget=%r, field=%r)'%(self.widget, self.field)
class FormFieldsWrapper(ObjectWrapper):
"""
Allow fields attr of a form to be accessed (as a generator) but also callable
"""
form = None
def __init__(self, form):
self.form = form
ObjectWrapper.__init__(self, form.structure.fields)
def __call__(self):
return self.form.renderer('/formish/form/fields.html', {'form':self.form})
class Form(object):
"""
The definition of a form
The Form type is the container for all the information a form needs to
render and validate data.
"""
SUPPORTED_METHODS = ['GET', 'POST']
renderer = _default_renderer
_element_name = None
_name = None
_request_data = None
def __init__(self, structure, name=None, defaults=None, errors=None,
action_url=None, renderer=None, method='POST', add_default_action=True, include_charset=True):
"""
Create a new form instance
:arg structure: Schema Structure attribute to bind to the the form
:type structure: schemaish.Structure
:arg name: Optional form name used to identify multiple forms on the same page
:type name: str "valid html id"
:arg defaults: Default values for the form
:type defaults: dict
:arg errors: Errors to store on the form for redisplay
:type errors: dict
:arg action_url: Use if you don't want the form to post to itself
:type action_url: string "url or path"
:arg renderer: Something that returns a form serialization when called
:type renderer: callable
:arg method: Option method, default POST
:type method: string
"""
if method.upper() not in self.SUPPORTED_METHODS:
raise ValueError("method must be one of GET or POST")
# allow a single schema items to be used on a form
if not isinstance(structure, schemaish.Structure):
structure = schemaish.Structure([structure])
self.structure = Group(None, structure, self)
self.item_data = {}
self.name = name
if defaults is None:
defaults = {}
if errors is None:
errors = {}
self.defaults = defaults
self.errors = errors
self.error = None
self._actions = []
if add_default_action:
self.add_action( None, 'Submit' )
self.action_url = action_url
if renderer is not None:
self.renderer = renderer
self.method = method
self.widget = widgets.StructureDefault()
self.include_charset = include_charset
def __repr__(self):
attributes = []
attributes.append('%r'%self.structure.attr)
attributes.append('name=%r'%self.name)
if self.defaults._o != {}:
attributes.append('defaults=%r'%self.defaults._o)
if self.errors != {}:
attributes.append('errors=%r'%self.errors)
if self.action_url:
attributes.append('action_url=%r'%self.action_url)
return 'formish.Form(%s)'%( ', '.join(attributes) )
def _element_name_get(self):
""" Set the element name """
return self._element_name
def _element_name_set(self, name):
""" Get the element name or raise an error """
if self._name is not None:
raise Exception("Named forms cannot be used as elements.")
self._element_name = name
element_name = property(_element_name_get, _element_name_set)
def add_action(self, name=None, value=None, callback=None):
"""
Add an action callable to the form
:arg callback: A function to call if this action is triggered
:type callback: callable
:arg name: The identifier for this action
:type name: string
:arg label: Use this label instead of the form.name for the value of
the action (for buttons, the value is used as the text on the button)
:type label: string
"""
if name and name in [action.name for action in self._actions]:
raise ValueError('Action with name %r already exists.'% name)
self._actions.append( Action(name, value, callback) )
def action(self, request, *args):
"""
Find and call the action callback for the action found in the request
:arg request: request which is used to find the action and also passed through to
the callback
:type request: webob.Request
:arg args: list of arguments Pass through to the callback
"""
if len(self._actions)==0:
raise validation.NoActionError('The form does not have any actions')
request_data = getattr(request, self.method.upper())
for action in self._actions:
if action.name in request_data:
return action.callback(request, self, *args)
return self._actions[0].callback(request, self, *args)
def get_unvalidated_data(self, request_data, raise_exceptions=True, skip_read_only_defaults=False):
"""
Convert the request object into a nested dict in the correct structure
of the schema but without applying the schema's validation.
:arg request_data: Webob style request data
:arg raise_exceptions: Whether to raise exceptions or return errors
"""
data = self.widget.from_request_data(self.structure, request_data, skip_read_only_defaults=skip_read_only_defaults)
if raise_exceptions and len(self.errors.keys()):
raise validation.FormError( \
'Tried to access data but conversion from request failed with %s errors (%s)'% \
(len(self.errors.keys()), self.errors))
return data
def _get_request_data(self):
"""
Retrieve previously set request_data or return the defaults in
request_data format.
"""
if self._request_data is not None:
return dotted(self._request_data)
self._request_data = dotted(self.widget.to_request_data(self.structure, self._defaults))
return dotted(self._request_data)
def _set_request_data(self, request_data):
"""
Assign raw request data to the form
:arg request_data: raw request data (e.g. request.POST)
:type request_data: Dictionary (dotted or nested or dotted or MultiDict)
"""
self._request_data = dotted(request_data)
request_data = property(_get_request_data, _set_request_data)
def _get_defaults(self):
""" Get the raw default data """
return dotted(self._defaults)
def _set_defaults(self, data):
""" assign data """
self._defaults = data
self._request_data = None
defaults = property(_get_defaults, _set_defaults)
def _set_request(self, request):
"""
Assign raw request data to the form
:arg request_data: raw request data (e.g. request.POST)
:type request_data: Dictionary (dotted or nested or dotted or MultiDict)
"""
self._request = request
request_data = getattr(request, self.method.upper())
# Decode request data according to the request's charset.
request_data = UnicodeMultiDict(request_data,
encoding=util.get_post_charset(request))
# Remove the sequence factory data from the request
for k in request_data.keys():
if '*' in k:
request_data.pop(k)
# We need the _request_data to be populated so sequences know how many
# items they have (i.e. .fields method on a sequence uses the number of
# values on the _request_data)
# Convert request data to a dottedish friendly representation
request_data = _unflatten_request_data(request_data)
self._request_data = dotted(request_data)
self._request_data = dotted(self.widget.pre_parse_incoming_request_data(self.structure,request_data))
def _get_request(self):
return self._request
request = property(_get_request, _set_request)
def name_from_request(self, request):
request_data = getattr(request, self.method.upper())
return request_data.get('__formish_form__')
def validate(self, request, failure_callable=None, success_callable=None, skip_read_only_defaults=False, check_form_name=True):
"""
Validate the form data in the request.
By default, this method returns either a dict of data or raises an
exception if validation fails. However, if either success_callable or
failure_callable are provided then the approriate callback will be
called, and the callback's result will be returned instead.
:arg request: the HTTP request
:type request: webob.Request
:arg failure_callable: Optional callback to call on failure.
:arg success_callable: Optional callback to call on success.
:returns: Python dict of converted and validated data.
:raises: formish.FormError, raised on validation failure.
"""
# Check this request was submitted by this form.
self.request = request
if check_form_name == True and (self.name != self.name_from_request(request)):
raise Exception("request does not match form name")
try:
data = self._validate(request, skip_read_only_defaults=skip_read_only_defaults)
except validation.FormError, e:
if failure_callable is None:
raise
else:
return failure_callable(request, self)
if success_callable is None:
return data
else:
return success_callable(request, data)
def _validate(self, request, skip_read_only_defaults=False):
"""
Get the data without raising exceptions and then validate the data. If
there are errors, raise them; otherwise return the data
"""
# XXX Should this happen after the basic stuff has happened?
self.errors = {}
data = self.get_unvalidated_data(self._request_data, raise_exceptions=False, skip_read_only_defaults=skip_read_only_defaults)
try:
self.structure.attr.validate(data)
except schemaish.attr.Invalid, e:
for key, value in e.error_dict.items():
if key not in self.errors:
self.errors[key] = value
if len(self.errors.keys()) > 0:
err_msg = 'Tried to access data but conversion from request failed with %s errors'
raise validation.FormError(err_msg% (len(self.errors.keys())))
return data
def set_item_data(self, key, name, value):
"""
Allow the setting os certain attributes on item_data, a dictionary used
to associates data with fields.
"""
allowed = ['title', 'widget', 'description','default']
if name in allowed:
if name == 'default' and '*' not in key:
set(self.defaults,key,value,container_factory=container_factory)
else:
self.item_data.setdefault(key, {})[name] = value
else:
raise KeyError('Cannot set data onto this attribute')
def get_item_data(self, key, name, default=NOARG):
"""
Access item data associates with a field key and an attribute name
(e.g. title, widget, description')
"""
if default is NOARG:
data = self.item_data.get(key, {})[name]
else:
data = self.item_data.get(key, {}).get(name, default)
return data
def get_item_data_values(self, name=None):
"""
get all of the item data values
"""
data = dotted({})
for key, value in self.item_data.items():
if name is not None and value.has_key(name):
data[key] = value[name]
else:
data[key] = value
return data
def __getitem__(self, key):
return FormAccessor(self, key)
@property
def fields(self):
"""
Return a generator that yields all of the fields at the top level of
the form (e.g. if a field is a subsection or sequence, it will be up to
the application to iterate that field's fields.
"""
return FormFieldsWrapper(self)
def get_field(self, name):
"""
Get a field by dotted field name
:arg name: Dotted name e.g. names.0.firstname
"""
segments = name.split('.')
for field in self.fields:
if field.name.split('.')[-1] == segments[0]:
if len(segments) == 1:
return field
else:
return field.get_field(segments[1:])
def __call__(self):
"""
Calling the Form generates a serialisation using the form's renderer
"""
return self.renderer('/formish/form/main.html', {'form':self})
def header(self):
""" Return just the header part of the template """
return self.renderer('/formish/form/header.html', {'form':self})
def footer(self):
""" Return just the footer part of the template """
return self.renderer('/formish/form/footer.html', {'form':self})
def metadata(self):
""" Return just the metada part of the template """
return self.renderer('/formish/form/metadata.html', {'form':self})
def error_list(self):
""" Return just the metada part of the template """
return self.renderer('/formish/form/error_list.html', {'form':self})
def actions(self):
""" Return just the actions part of the template """
return self.renderer('/formish/form/actions.html', {'form':self})
def _unflatten_request_data(request_data):
"""
Unflatten the request data into nested dicts and lists.
"""
# Build an ordered list of keys. Don't rely on the request_data doing this
# for us because webob's MultiDict yields the same key multiple times!
# Of course, if request_data is not an ordered dict then this is fairly
# pointless anyway.
keys = []
for key in request_data:
if key not in keys:
keys.append(key)