web/form.py
branchtls-sprint
changeset 907 192800415f59
parent 905 64fd6eaaa9a4
child 908 136d91725ecf
child 910 a86ab461b8fd
equal deleted inserted replaced
906:c26156f0885e 907:192800415f59
     6 """
     6 """
     7 __docformat__ = "restructuredtext en"
     7 __docformat__ = "restructuredtext en"
     8 
     8 
     9 from warnings import warn
     9 from warnings import warn
    10 from simplejson import dumps
    10 from simplejson import dumps
    11 from mx.DateTime import today
    11 from mx.DateTime import today, now
    12 
    12 
    13 from logilab.common.compat import any
    13 from logilab.common.compat import any
    14 from logilab.mtconverter import html_escape
    14 from logilab.mtconverter import html_escape
    15 
    15 
    16 from yams.constraints import SizeConstraint, StaticVocabularyConstraint
    16 from yams.constraints import SizeConstraint, StaticVocabularyConstraint
    18 from cubicweb import typed_eid
    18 from cubicweb import typed_eid
    19 from cubicweb.utils import ustrftime
    19 from cubicweb.utils import ustrftime
    20 from cubicweb.selectors import match_form_params
    20 from cubicweb.selectors import match_form_params
    21 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
    21 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
    22 from cubicweb.common.registerers import accepts_registerer
    22 from cubicweb.common.registerers import accepts_registerer
       
    23 from cubicweb.common.uilib import toggle_action
    23 from cubicweb.web import stdmsgs
    24 from cubicweb.web import stdmsgs
    24 from cubicweb.web.httpcache import NoHTTPCacheManager
    25 from cubicweb.web.httpcache import NoHTTPCacheManager
    25 from cubicweb.web.controller import NAV_FORM_PARAMETERS, redirect_params
    26 from cubicweb.web.controller import NAV_FORM_PARAMETERS, redirect_params
    26 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
    27 from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
    27 
    28 
   266         
   267         
   267     def render(self, form, field):
   268     def render(self, form, field):
   268         raise NotImplementedError
   269         raise NotImplementedError
   269 
   270 
   270     def _render_attrs(self, form, field):
   271     def _render_attrs(self, form, field):
   271         # name = form.form_field_name(field)
       
   272         # values = form.form_field_value(field)
       
   273         name = form.context[field]['name']
   272         name = form.context[field]['name']
   274         values = form.context[field]['value']
   273         values = form.context[field]['value']
   275         if not isinstance(values, (tuple, list)):
   274         if not isinstance(values, (tuple, list)):
   276             values = (values,)
   275             values = (values,)
       
   276         attrs = dict(self.attrs)
       
   277         attrs['id'] = form.context[field]['id']
   277         return name, values, dict(self.attrs)
   278         return name, values, dict(self.attrs)
   278 
   279 
   279 class Input(FieldWidget):
   280 class Input(FieldWidget):
   280     type = None
   281     type = None
   281     
   282     
   289 class TextInput(Input):
   290 class TextInput(Input):
   290     type = 'text'
   291     type = 'text'
   291 
   292 
   292 class PasswordInput(Input):
   293 class PasswordInput(Input):
   293     type = 'password'
   294     type = 'password'
       
   295     # XXX password validation
   294 
   296 
   295 class FileInput(Input):
   297 class FileInput(Input):
   296     type = 'file'
   298     type = 'file'
   297 
   299     
       
   300     def _render_attrs(self, form, field):
       
   301         # ignore value which makes no sense here (XXX even on form validation error?)
       
   302         name, values, attrs = super(FileInput, self)._render_attrs(form, field)
       
   303         return name, ('',), attrs
       
   304         
   298 class HiddenInput(Input):
   305 class HiddenInput(Input):
   299     type = 'hidden'
   306     type = 'hidden'
   300     
   307     
   301 class Button(Input):
   308 class ButtonInput(Input):
   302     type = 'button'
   309     type = 'button'
   303 
   310 
   304 class TextArea(FieldWidget):
   311 class TextArea(FieldWidget):
   305     def render(self, form, field):
   312     def render(self, form, field):
   306         name, values, attrs = self._render_attrs(form, field)
   313         name, values, attrs = self._render_attrs(form, field)
   310         elif len(values) == 1:
   317         elif len(values) == 1:
   311             value = values[0]
   318             value = values[0]
   312         else:
   319         else:
   313             raise ValueError('a textarea is not supposed to be multivalued')
   320             raise ValueError('a textarea is not supposed to be multivalued')
   314         return tags.textarea(value, name=name, **attrs)
   321         return tags.textarea(value, name=name, **attrs)
       
   322 
   315 
   323 
   316 class FCKEditor(TextArea):
   324 class FCKEditor(TextArea):
   317     def __init__(self, attrs):
   325     def __init__(self, attrs):
   318         super(FCKEditor, self).__init__(attrs)
   326         super(FCKEditor, self).__init__(attrs)
   319         self.attrs['cubicweb:type'] = 'wysiwyg'
   327         self.attrs['cubicweb:type'] = 'wysiwyg'
   345             return tags.select(name=name, options=options)
   353             return tags.select(name=name, options=options)
   346         return tags.select(name=name, multiple=self.multiple,
   354         return tags.select(name=name, multiple=self.multiple,
   347                            options=options, **attrs)
   355                            options=options, **attrs)
   348 
   356 
   349 
   357 
   350 class CheckBox(FieldWidget):
   358 class CheckBox(Input):
   351 
   359     type = 'checkbox'
       
   360     
   352     def _render_attrs(self, form, field):
   361     def _render_attrs(self, form, field):
   353         name, value, attrs = super(CheckBox, self)._render_attrs(form, field)
   362         name, values, attrs = super(CheckBox, self)._render_attrs(form, field)
   354         if value:
   363         if values and values[0]:
   355             attrs['checked'] = u'checked'
   364             attrs['checked'] = u'checked'
   356         return name, None, attrs
   365         return name, values, attrs
   357 
   366 
   358         
   367         
   359 class Radio(FieldWidget):
   368 class Radio(FieldWidget):
   360     pass
   369     pass
   361 
   370 
   362 
   371 
   363 class DateTimePicker(TextInput):
   372 class DateTimePicker(TextInput):
   364     monthnames = ("january", "february", "march", "april",
   373     monthnames = ('january', 'february', 'march', 'april',
   365                   "may", "june", "july", "august",
   374                   'may', 'june', 'july', 'august',
   366                   "september", "october", "november", "december")
   375                   'september', 'october', 'november', 'december')
   367     
   376     daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
   368     daynames = ("monday", "tuesday", "wednesday", "thursday",
   377                 'friday', 'saturday', 'sunday')
   369                 "friday", "saturday", "sunday")
       
   370 
   378 
   371     needs_js = ('cubicweb.ajax.js', 'cubicweb.calendar.js')
   379     needs_js = ('cubicweb.ajax.js', 'cubicweb.calendar.js')
   372     needs_css = ('cubicweb.calendar_popup.css',)
   380     needs_css = ('cubicweb.calendar_popup.css',)
   373     
   381     
   374     @classmethod
   382     @classmethod
   461     def format_single_value(self, req, value):
   469     def format_single_value(self, req, value):
   462         if value is None:
   470         if value is None:
   463             return u''
   471             return u''
   464         return unicode(value)
   472         return unicode(value)
   465 
   473 
   466     def get_widget(self, req):
   474     def get_widget(self, form):
   467         return self.widget
   475         return self.widget
   468 
   476     
   469     def render(self, form):
   477     def example_format(self, req):
   470         return self.get_widget(form.req).render(form, self)
   478         return u''
       
   479 
       
   480     def render(self, form, renderer):
       
   481         return self.get_widget(form).render(form, self)
   471 
   482 
   472 
   483 
   473     def vocabulary(self, form):
   484     def vocabulary(self, form):
   474         return self.choices
   485         return self.choices
       
   486 
   475     
   487     
   476 class StringField(Field):
   488 class StringField(Field):
   477     def __init__(self, max_length=None, **kwargs):
   489     def __init__(self, max_length=None, **kwargs):
   478         super(StringField, self).__init__(**kwargs)
   490         super(StringField, self).__init__(**kwargs)
   479         self.max_length = max_length
   491         self.max_length = max_length
   480 
   492 
       
   493 
   481 class TextField(Field):
   494 class TextField(Field):
       
   495     widget = TextArea
   482     def __init__(self, rows=10, cols=80, **kwargs):
   496     def __init__(self, rows=10, cols=80, **kwargs):
   483         widget = TextArea(dict(rows=rows, cols=cols))
   497         super(TextField, self).__init__(**kwargs)
   484         super(TextField, self).__init__(widget=widget, **kwargs)
       
   485         self.rows = rows
   498         self.rows = rows
   486         self.cols = cols
   499         self.cols = cols
       
   500 
   487 
   501 
   488 class RichTextField(TextField):
   502 class RichTextField(TextField):
   489     widget = None
   503     widget = None
   490     def __init__(self, format_field=None, **kwargs):
   504     def __init__(self, format_field=None, **kwargs):
   491         super(RichTextField, self).__init__(**kwargs)
   505         super(RichTextField, self).__init__(**kwargs)
   492         self.format_field = format_field
   506         self.format_field = format_field
   493 
   507 
   494     def get_widget(self, req):
   508     def get_widget(self, form):
   495         if self.widget is None:
   509         if self.widget is None:
   496             if self.use_fckeditor(req):
   510             if self.use_fckeditor(form):
   497                 return FCKEditor()
   511                 return FCKEditor()
   498             return TextArea()
   512             return TextArea()
   499         return self.widget
   513         return self.widget
   500 
   514 
   501     def get_format_field(self, form):
   515     def get_format_field(self, form):
   502         if not self.format_field:
   516         if self.format_field:
   503             # if fckeditor is used and format field isn't explicitly
   517             return self.format_field
   504             # deactivated, we want an hidden field for the format
   518         # we have to cache generated field since it's use as key in the
       
   519         # context dictionnary
       
   520         try:
       
   521             return form.req.data[self]
       
   522         except KeyError:
   505             if self.use_fckeditor(form):
   523             if self.use_fckeditor(form):
   506                 widget = HiddenInput
   524                 # if fckeditor is used and format field isn't explicitly
   507             # else we want a format selector
   525                 # deactivated, we want an hidden field for the format
       
   526                 widget = HiddenInput()
   508             else:
   527             else:
       
   528                 # else we want a format selector
       
   529                 # XXX compute vocabulary
   509                 widget = Select
   530                 widget = Select
   510             return StringField(name=self.name + '_format', widget=widget)
   531             field = StringField(name=self.name + '_format', widget=widget)
   511         else:
   532             form.req.data[self] = field
   512             return self.format_field
   533             return field
   513     
   534     
   514     def actual_fields(self, form):
   535     def actual_fields(self, form):
   515         yield self
   536         yield self
   516         format_field = self.get_format_field(form)
   537         format_field = self.get_format_field(form)
   517         if format_field:
   538         if format_field:
   523         """
   544         """
   524         if form.config.fckeditor_installed() and form.req.property_value('ui.fckeditor'):
   545         if form.config.fckeditor_installed() and form.req.property_value('ui.fckeditor'):
   525             return form.form_format_field_value(self) == 'text/html'
   546             return form.form_format_field_value(self) == 'text/html'
   526         return False
   547         return False
   527 
   548 
   528     def render(self, form):
   549     def render(self, form, renderer):
   529         format_field = self.get_format_field(form)
   550         format_field = self.get_format_field(form)
   530         if format_field:
   551         if format_field:
   531             result = format_field.render(form)
   552             result = format_field.render(form, renderer)
   532         else:
   553         else:
   533             result = u''
   554             result = u''
   534         return result + self.get_widget(form.req).render(form, self)
   555         return result + self.get_widget(form).render(form, self)
   535 
   556 
       
   557     
       
   558 class FileField(StringField):
       
   559     widget = FileInput
       
   560     needs_multipart = True
       
   561     
       
   562     def __init__(self, format_field=None, encoding_field=None, **kwargs):
       
   563         super(FileField, self).__init__(**kwargs)
       
   564         self.format_field = format_field
       
   565         self.encoding_field = encoding_field
       
   566         
       
   567     def actual_fields(self, form):
       
   568         yield self
       
   569         if self.format_field:
       
   570             yield self.format_field
       
   571         if self.encoding_field:
       
   572             yield self.encoding_field
       
   573 
       
   574     def render(self, form, renderer):
       
   575         wdgs = [self.get_widget(form).render(form, self)]
       
   576         if self.format_field or self.encoding_field:
       
   577             divid = '%s-advanced' % form.context[self]['name']
       
   578             wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
       
   579                         (html_escape(toggle_action(divid)),
       
   580                          form.req._('show advanced fields'),
       
   581                          html_escape(form.req.build_url('data/puce_down.png')),
       
   582                          form.req._('show advanced fields')))
       
   583             wdgs.append(u'<div id="%s" class="hidden">' % divid)
       
   584             if self.format_field:
       
   585                 wdgs.append(self.render_subfield(form, self.format_field, renderer))
       
   586             if self.encoding_field:
       
   587                 wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
       
   588             wdgs.append(u'</div>')            
       
   589         if not self.required and form.context[self]['value']:
       
   590             # trick to be able to delete an uploaded file
       
   591             wdgs.append(u'<br/>')
       
   592             wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'],
       
   593                                    type=u'checkbox'))
       
   594             wdgs.append(form.req._('detach attached file'))
       
   595         return u'\n'.join(wdgs)
       
   596 
       
   597     def render_subfield(self, form, field, renderer):
       
   598         return (renderer.render_label(form, field)
       
   599                 + field.render(form, renderer)
       
   600                 + renderer.render_help(form, field)
       
   601                 + u'<br/>')
       
   602         
   536     
   603     
   537 class IntField(Field):
   604 class IntField(Field):
   538     def __init__(self, min=None, max=None, **kwargs):
   605     def __init__(self, min=None, max=None, **kwargs):
   539         super(IntField, self).__init__(**kwargs)
   606         super(IntField, self).__init__(**kwargs)
   540         self.min = min
   607         self.min = min
   541         self.max = max
   608         self.max = max
   542 
   609 
       
   610 
   543 class BooleanField(Field):
   611 class BooleanField(Field):
   544     widget = Radio
   612     widget = Radio
       
   613 
   545 
   614 
   546 class FloatField(IntField):    
   615 class FloatField(IntField):    
   547     def format_single_value(self, req, value):
   616     def format_single_value(self, req, value):
   548         formatstr = entity.req.property_value('ui.float-format')
   617         formatstr = entity.req.property_value('ui.float-format')
   549         if value is None:
   618         if value is None:
   550             return u''
   619             return u''
   551         return formatstr % float(value)
   620         return formatstr % float(value)
   552 
   621 
       
   622     def render_example(self, req):
       
   623         return self.format_value(req, 1.234)
       
   624 
       
   625 
   553 class DateField(StringField):
   626 class DateField(StringField):
   554     format_prop = 'ui.date-format'
   627     format_prop = 'ui.date-format'
   555     widget = DateTimePicker
   628     widget = DateTimePicker
   556     
   629     
   557     def format_single_value(self, req, value):
   630     def format_single_value(self, req, value):
   558         return value and ustrftime(value, req.property_value(self.format_prop)) or u''
   631         return value and ustrftime(value, req.property_value(self.format_prop)) or u''
   559 
   632 
       
   633     def render_example(self, req):
       
   634         return self.format_value(req, now())
       
   635 
       
   636 
   560 class DateTimeField(DateField):
   637 class DateTimeField(DateField):
   561     format_prop = 'ui.datetime-format'
   638     format_prop = 'ui.datetime-format'
   562 
   639 
       
   640 
   563 class TimeField(DateField):
   641 class TimeField(DateField):
   564     format_prop = 'ui.datetime-format'
   642     format_prop = 'ui.datetime-format'
   565     
   643 
   566 class FileField(StringField):
       
   567     needs_multipart = True
       
   568 
   644 
   569 class HiddenInitialValueField(Field):
   645 class HiddenInitialValueField(Field):
   570     def __init__(self, visible_field, name):
   646     def __init__(self, visible_field, name):
   571         super(HiddenInitialValueField, self).__init__(name=name,
   647         super(HiddenInitialValueField, self).__init__(name=name,
   572                                                       widget=HiddenInput,
   648                                                       widget=HiddenInput,
   772             value = self.entity.eid
   848             value = self.entity.eid
   773         elif fieldname in values:
   849         elif fieldname in values:
   774             value = values[fieldname]
   850             value = values[fieldname]
   775         elif fieldname in self.req.form:
   851         elif fieldname in self.req.form:
   776             value = self.req.form[fieldname]
   852             value = self.req.form[fieldname]
   777         elif isinstance(field, FileField):
       
   778             return None # XXX manage FileField
       
   779         else:
   853         else:
   780             if self.entity.has_eid() and field.eidparam:
   854             if self.entity.has_eid() and field.eidparam:
   781                 # use value found on the entity or field's initial value if it's
   855                 # use value found on the entity or field's initial value if it's
   782                 # not an attribute of the entity (XXX may conflicts and get
   856                 # not an attribute of the entity (XXX may conflicts and get
   783                 # undesired value)
   857                 # undesired value)
   808 
   882 
   809     def form_field_entity_value(self, field, default_initial=True):
   883     def form_field_entity_value(self, field, default_initial=True):
   810         attr = field.name 
   884         attr = field.name 
   811         if field.role == 'object':
   885         if field.role == 'object':
   812             attr += '_object'
   886             attr += '_object'
       
   887         else:
       
   888             attrtype = self.entity.e_schema.destination(attr)
       
   889             if attrtype == 'Password':
       
   890                 return self.entity.has_eid() and INTERNAL_FIELD_VALUE or ''
       
   891             if attrtype == 'Bytes':
       
   892                 # XXX value should reflect if some file is already attached
       
   893                 return self.entity.has_eid()
   813         if default_initial:
   894         if default_initial:
   814             value = getattr(self.entity, attr, field.initial)
   895             value = getattr(self.entity, attr, field.initial)
   815         else:
   896         else:
   816             value = getattr(self.entity, attr)
   897             value = getattr(self.entity, attr)
   817         if isinstance(field, RelationField):
   898         if isinstance(field, RelationField):
   901                 continue
   982                 continue
   902             done.add(entity.eid)
   983             done.add(entity.eid)
   903             res.append((entity.view('combobox'), entity.eid))
   984             res.append((entity.view('combobox'), entity.eid))
   904         return res
   985         return res
   905 
   986 
   906     
       
   907 
   987 
   908 class MultipleFieldsForm(FieldsForm):
   988 class MultipleFieldsForm(FieldsForm):
   909     def __init__(self, *args, **kwargs):
   989     def __init__(self, *args, **kwargs):
   910         super(MultipleFieldsForm, self).__init__(*args, **kwargs)
   990         super(MultipleFieldsForm, self).__init__(*args, **kwargs)
   911         self.forms = []
   991         self.forms = []
   912 
   992 
   913     def form_add_subform(self, subform):
   993     def form_add_subform(self, subform):
   914         self.forms.append(subform)
   994         self.forms.append(subform)
   915         
   995 
       
   996 
   916 # form renderers ############
   997 # form renderers ############
   917 class FormRenderer(object):
   998 class FormRenderer(object):
   918     
   999 
   919     def render(self, form, values):
  1000     # renderer interface ######################################################
       
  1001     
       
  1002     def render(self, form, values, display_help=True):
   920         data = []
  1003         data = []
   921         w = data.append
  1004         w = data.append
   922         # XXX form_needs_multipart
       
   923         w(self.open_form(form))
  1005         w(self.open_form(form))
   924         w(u'<div id="progress">%s</div>' % form.req._('validating...'))
  1006         w(u'<div id="progress">%s</div>' % form.req._('validating...'))
   925         w(u'<fieldset>')
  1007         w(u'<fieldset>')
   926         w(tags.input(type='hidden', name='__form_id', value=form.domid))
  1008         w(tags.input(type='hidden', name='__form_id', value=form.domid))
   927         if form.redirect_path:
  1009         if form.redirect_path:
   928             w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
  1010             w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
   929         self.render_fields(w, form, values)
  1011         self.render_fields(w, form, values, display_help)
   930         self.render_buttons(w, form)
  1012         self.render_buttons(w, form)
   931         w(u'</fieldset>')
  1013         w(u'</fieldset>')
   932         w(u'</form>')
  1014         w(u'</form>')
   933         return '\n'.join(data)
  1015         return '\n'.join(data)
   934 
  1016         
       
  1017     def render_label(self, form, field):
       
  1018         label = form.req._(field.label)
       
  1019         attrs = {'for': form.context[field]['id']}
       
  1020         if field.required:
       
  1021             attrs['class'] = 'required'
       
  1022         return tags.label(label, **attrs)
       
  1023 
       
  1024     def render_help(self, form, field):
       
  1025         help = [ u'<br/>' ]
       
  1026         descr = field.help
       
  1027         if descr:
       
  1028             help.append('<span class="helper">%s</span>' % req._(descr))
       
  1029         example = field.example_format(form.req)
       
  1030         if example:
       
  1031             help.append('<span class="helper">(%s: %s)</span>'
       
  1032                         % (req._('sample format'), example))
       
  1033         return u'&nbsp;'.join(help)
       
  1034 
       
  1035     # specific methods (mostly to ease overriding) #############################
       
  1036     
   935     def open_form(self, form):
  1037     def open_form(self, form):
   936         if form.form_needs_multipart:
  1038         if form.form_needs_multipart:
   937             enctype = 'multipart/form-data'
  1039             enctype = 'multipart/form-data'
   938         else:
  1040         else:
   939             enctype = 'application/x-www-form-urlencoded'
  1041             enctype = 'application/x-www-form-urlencoded'
   947             tag += ' class="%s"' % html_escape(form.cssclass)
  1049             tag += ' class="%s"' % html_escape(form.cssclass)
   948         if form.cwtarget:
  1050         if form.cwtarget:
   949             tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
  1051             tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
   950         return tag + '>'
  1052         return tag + '>'
   951         
  1053         
   952     def render_fields(self, w, form, values):
  1054     def render_fields(self, w, form, values, display_help=True):
   953         form.form_build_context(values)
  1055         form.form_build_context(values)
   954         fields = form.fields[:]
  1056         fields = form.fields[:]
   955         for field in form.fields:
  1057         for field in form.fields:
   956             if not field.is_visible():
  1058             if not field.is_visible():
   957                 w(field.render(form))
  1059                 w(field.render(form, self))
   958                 fields.remove(field)
  1060                 fields.remove(field)
   959         if fields:
  1061         if fields:
   960             w(u'<table>')
  1062             w(u'<table>')
   961             for field in fields:
  1063             for field in fields:
   962                 w(u'<tr>')
  1064                 w(u'<tr>')
   963                 w('<th>%s</th>' % self.render_label(form, field))
  1065                 w('<th>%s</th>' % self.render_label(form, field))
   964                 w(u'<td style="width:100%;">')
  1066                 w(u'<td style="width:100%;">')
   965                 w(field.render(form))
  1067                 w(field.render(form, self))
       
  1068                 if display_help == True:
       
  1069                     w(self.render_help(form, field))
   966                 w(u'</td></tr>')
  1070                 w(u'</td></tr>')
   967             w(u'</table>')
  1071             w(u'</table>')
   968         for childform in getattr(form, 'forms', []):
  1072         for childform in getattr(form, 'forms', []):
   969             self.render_fields(w, childform, values)
  1073             self.render_fields(w, childform, values)
   970 
       
   971     #def render_field(self, w, form, field):
       
   972         
  1074         
   973     def render_buttons(self, w, form):
  1075     def render_buttons(self, w, form):
   974         w(u'<table class="formButtonBar">\n<tr>\n')
  1076         w(u'<table class="formButtonBar">\n<tr>\n')
   975         for button in form.form_buttons():
  1077         for button in form.form_buttons():
   976             w(u'<td>%s</td>\n' % button)
  1078             w(u'<td>%s</td>\n' % button)
   977         w(u'</tr></table>')
  1079         w(u'</tr></table>')
   978         
       
   979     def render_label(self, form, field):
       
   980         label = form.req._(field.label)
       
   981         attrs = {'for': form.context[field]['id']}
       
   982         if field.required:
       
   983             attrs['class'] = 'required'
       
   984         return tags.label(label, **attrs)
       
   985 
  1080 
   986 
  1081 
   987 def stringfield_from_constraints(constraints, **kwargs):
  1082 def stringfield_from_constraints(constraints, **kwargs):
   988     field = None
  1083     field = None
   989     for cstr in constraints:
  1084     for cstr in constraints: