web/form.py
branchtls-sprint
changeset 844 8ab6f64c3750
parent 765 8fda14081686
child 847 27c4ebe90d03
equal deleted inserted replaced
843:5676811ef760 844:8ab6f64c3750
     6 """
     6 """
     7 __docformat__ = "restructuredtext en"
     7 __docformat__ = "restructuredtext en"
     8 
     8 
     9 from simplejson import dumps
     9 from simplejson import dumps
    10 
    10 
       
    11 from logilab.common.compat import any
    11 from logilab.mtconverter import html_escape
    12 from logilab.mtconverter import html_escape
    12 
    13 
    13 from cubicweb import typed_eid
    14 from cubicweb import typed_eid
    14 from cubicweb.selectors import match_form_params
    15 from cubicweb.selectors import match_form_params
    15 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
    16 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
    16 from cubicweb.common.registerers import accepts_registerer
    17 from cubicweb.common.registerers import accepts_registerer
    17 from cubicweb.web import stdmsgs
    18 from cubicweb.web import stdmsgs
    18 from cubicweb.web.httpcache import NoHTTPCacheManager
    19 from cubicweb.web.httpcache import NoHTTPCacheManager
    19 from cubicweb.web.controller import redirect_params
    20 from cubicweb.web.controller import redirect_params
       
    21 from cubicweb.web import eid_param
    20 
    22 
    21 
    23 
    22 def relation_id(eid, rtype, target, reid):
    24 def relation_id(eid, rtype, target, reid):
    23     if target == 'subject':
    25     if target == 'subject':
    24         return u'%s:%s:%s' % (eid, rtype, reid)
    26         return u'%s:%s:%s' % (eid, rtype, reid)
    25     return u'%s:%s:%s' % (reid, rtype, eid)
    27     return u'%s:%s:%s' % (reid, rtype, eid)
       
    28         
       
    29 def toggable_relation_link(eid, nodeid, label='x'):
       
    30     js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
       
    31     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
    26 
    32 
    27 
    33 
    28 class FormMixIn(object):
    34 class FormMixIn(object):
    29     """abstract form mix-in"""
    35     """abstract form mix-in"""
    30     category = 'form'
    36     category = 'form'
   225     
   231     
   226     def button_reset(self, label=None, tabindex=None):
   232     def button_reset(self, label=None, tabindex=None):
   227         label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
   233         label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
   228         return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
   234         return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
   229             label, tabindex or 4)
   235             label, tabindex or 4)
   230         
   236 
   231 def toggable_relation_link(eid, nodeid, label='x'):
   237 
   232     js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
   238 ###############################################################################
   233     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
   239 
   234 
   240 from cubicweb.common import tags
       
   241 
       
   242 # widgets ############
       
   243 
       
   244 class FieldWidget(object):
       
   245     def __init__(self, attrs=None):
       
   246         self.attrs = attrs or {}
       
   247     
       
   248     def render(self, form, field):
       
   249         raise NotImplementedError
       
   250     
       
   251 class Input(FieldWidget):
       
   252     type = None
       
   253     
       
   254     def render(self, form, field):
       
   255         name, value, attrs = self._render_attrs(form, field)
       
   256         if attrs is None:
       
   257             return tags.input(name=name, value=value)
       
   258         return tags.input(name=name, value=value, type=self.type, **attrs)
       
   259 
       
   260     def _render_attrs(self, form, field):
       
   261         name = form.context[field]['name'] # qualified name
       
   262         value = form.context[field]['value']
       
   263         #fattrs = field.widget_attributes(self)
       
   264         attrs = self.attrs.copy()
       
   265         #attrs.update(fattrs)
       
   266         # XXX id
       
   267         return name, value, attrs
       
   268     
       
   269 class TextInput(Input):
       
   270     type = 'text'
       
   271 
       
   272 class PasswordInput(Input):
       
   273     type = 'password'
       
   274 
       
   275 class FileInput(Input):
       
   276     type = 'file'
       
   277 
       
   278 class HiddenInput(Input):
       
   279     type = 'hidden'
       
   280 
       
   281 class Button(Input):
       
   282     type = 'button'
       
   283 
       
   284 class TextArea(FieldWidget):
       
   285     def render(self, form, field):
       
   286         name, value, attrs = self._render_attrs(form, field)
       
   287         if attrs is None:
       
   288             return tags.textarea(value, name=name)
       
   289         return tags.textarea(value, name=name, **attrs)
       
   290 
       
   291 class Select: 
       
   292     def render(self, form, field):
       
   293         name, value, attrs = self._render_attrs(form, field)
       
   294         if self.vocabulary:
       
   295             # static vocabulary defined in form definition
       
   296             vocab = self.vocabulary
       
   297         else:
       
   298             vocab = form.get_vocabulary(field)
       
   299         options = []
       
   300         for label, value in vocab:
       
   301             options.append(tags.option(label, value=value))
       
   302         if attrs is None:
       
   303             return tags.select(name=name, options=options)
       
   304         return tags.select(name=name, options=options, **attrs)
       
   305 
       
   306 class CheckBox: pass
       
   307 
       
   308 class Radio: pass
       
   309 
       
   310 class DateTimePicker: pass
       
   311 
       
   312 
       
   313 # fields ############
       
   314 
       
   315 class Field(object):
       
   316     """field class is introduced to control what's displayed in edition form
       
   317     """
       
   318     widget = TextInput
       
   319     needs_multipart = False
       
   320     creation_rank = 0
       
   321     
       
   322     def __init__(self, name=None, id=None, label=None,
       
   323                  widget=None, required=False, initial=None, help=None,
       
   324                  eidparam=True):
       
   325         self.required = required
       
   326         if widget is not None:
       
   327             self.widget = widget
       
   328         if isinstance(self.widget, type):
       
   329             self.widget = self.widget()
       
   330         self.name = name
       
   331         self.label = label or name
       
   332         self.id = id or name
       
   333         self.initial = initial
       
   334         self.help = help
       
   335         self.eidparam = eidparam
       
   336         # global fields ordering in forms
       
   337         Field.creation_rank += 1
       
   338 
       
   339     def set_name(self, name):
       
   340         self.name = name
       
   341         if not self.id:
       
   342             self.id = name
       
   343         if not self.label:
       
   344             self.label = name
       
   345 
       
   346     def format_value(self, req, value):
       
   347         return unicode(value)
       
   348 
       
   349     def render(self, form):
       
   350         return self.widget.render(form, self)
       
   351 
       
   352 
       
   353 class StringField(Field):
       
   354     def __init__(self, max_length=None, **kwargs):
       
   355         super(StringField, self).__init__(**kwargs)
       
   356         self.max_length = max_length
       
   357         
       
   358 class TextField(Field):
       
   359     widget = TextArea
       
   360     def __init__(self, row=None, col=None, **kwargs):
       
   361         super(TextField, self).__init__(**kwargs)
       
   362         self.row = row
       
   363         self.col = col
       
   364 
       
   365 class RichTextField(Field):
       
   366     pass
       
   367 
       
   368 class IntField(Field):
       
   369     def __init__(self, min=None, max=None, **kwargs):
       
   370         super(IntField, self).__init__(**kwargs)
       
   371         self.min = min
       
   372         self.max = max
       
   373 
       
   374 class FloatField(IntField):
       
   375     
       
   376     def format_value(self, req, value):
       
   377         if value is not None:
       
   378             return ustrftime(value, req.property_value('ui.float-format'))
       
   379         return u''
       
   380 
       
   381 class DateField(IntField):
       
   382     
       
   383     def format_value(self, req, value):
       
   384         return value and ustrftime(value, req.property_value('ui.date-format')) or u''
       
   385 
       
   386 class DateTimeField(IntField):
       
   387 
       
   388     def format_value(self, req, value):
       
   389         return value and ustrftime(value, req.property_value('ui.datetime-format')) or u''
       
   390 
       
   391 class FileField(IntField):
       
   392     needs_multipart = True
       
   393                  
       
   394 # forms ############
       
   395 class metafieldsform(type):
       
   396     def __new__(mcs, name, bases, classdict):
       
   397         allfields = []
       
   398         for base in bases:
       
   399             if hasattr(base, '_fields_'):
       
   400                 allfields += base._fields_
       
   401         clsfields = (item for item in classdict.items()
       
   402                      if isinstance(item[1], Field))
       
   403         for name, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
       
   404             if not field.name:
       
   405                 field.set_name(name)
       
   406             allfields.append(field)
       
   407         classdict['_fields_'] = allfields
       
   408         return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict)
       
   409     
       
   410 
       
   411 class FieldsForm(object):
       
   412     __metaclass__ = metafieldsform
       
   413     
       
   414     def __init__(self, req, id=None, title=None, action='edit',
       
   415                  redirect_path=None):
       
   416         self.req = req
       
   417         self.id = id or 'form'
       
   418         self.title = title
       
   419         self.action = action
       
   420         self.redirect_path = None
       
   421         self.fields = list(self.__class__._fields_)
       
   422         self.context = {}
       
   423         
       
   424     @property
       
   425     def needs_multipart(self):
       
   426         return any(field.needs_multipart for field in self.fields) 
       
   427 
       
   428     def render(self, **values):
       
   429         renderer = values.pop('renderer', FormRenderer())
       
   430         self.build_context(values)
       
   431         return renderer.render(self)
       
   432 
       
   433     def build_context(self, values):
       
   434         self.context = context = {}
       
   435         for name, field in self.fields:
       
   436             value = values.get(field.name, field.initial)
       
   437             context[field] = {'value': field.format_value(self.req, value)}
       
   438 
       
   439     def get_vocabulary(self, field):
       
   440         raise NotImplementedError
       
   441 
       
   442     
       
   443 class EntityFieldsForm(FieldsForm):
       
   444     def __init__(self, *args, **kwargs):
       
   445         kwargs.setdefault('id', 'entityForm')
       
   446         super(EntityFieldsForm, self).__init__(*args, **kwargs)
       
   447         self.fields.append(TextField(name='__type', widget=HiddenInput))
       
   448         self.fields.append(TextField(name='eid', widget=HiddenInput))
       
   449         
       
   450     def render(self, entity, **values):
       
   451         self.entity = entity
       
   452         return super(EntityFieldsForm, self).render(**values)
       
   453 
       
   454     def build_context(self, values):
       
   455         self.context = context = {}
       
   456         for field in self.fields:
       
   457             try:
       
   458                 value = values[field.name]
       
   459             except KeyError:
       
   460                 value = getattr(self.entity, field.name, field.initial)
       
   461             if field.eidparam:
       
   462                 name = eid_param(field.name, self.entity.eid)
       
   463             else:
       
   464                 name = field.name
       
   465             context[field] = {'value': field.format_value(self.req, value),
       
   466                               'name': name}
       
   467         
       
   468     def get_vocabulary(self, field):
       
   469         choices = self.vocabfunc(entity)
       
   470         if self.sort:
       
   471             choices = sorted(choices)
       
   472         if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'):
       
   473             return zip((entity.req._(v) for v in choices), choices)
       
   474         return zip(choices, choices)
       
   475 
       
   476     
       
   477 # form renderers ############
       
   478 
       
   479 class FormRenderer(object):
       
   480     def render(self, form):
       
   481         data = []
       
   482         w = data.append
       
   483         w(u'<form action="%s" onsubmit="return freezeFormButtons(\'%s\');" method="post" id="%s">'
       
   484           % (form.req.build_url(form.action), form.id, form.id))
       
   485         w(u'<div id="progress">%s</div>' % _('validating...'))
       
   486         w(u'<fieldset>')
       
   487         w(tags.input(type='hidden', name='__form_id', value=form.id))
       
   488         if form.redirect_path:
       
   489             w(tags.input(type='hidden', name='__redirect_path', value=form.redirect_path))
       
   490         for field in form.fields:
       
   491             w(field.render(form))
       
   492         for button in form.buttons():
       
   493             w(button.render())
       
   494         w(u'</fieldset>')
       
   495         w(u'</form>')
       
   496         return '\n'.join(data)