web/formfields.py
branchtls-sprint
changeset 1081 f2a85f52b9e5
child 1095 6917ebe281e9
equal deleted inserted replaced
1074:c07f3accf04a 1081:f2a85f52b9e5
       
     1 """field classes for form construction
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 
       
     9 class Field(object):
       
    10     """field class is introduced to control what's displayed in edition form
       
    11     """
       
    12     widget = TextInput
       
    13     needs_multipart = False
       
    14     creation_rank = 0
       
    15 
       
    16     def __init__(self, name=None, id=None, label=None,
       
    17                  widget=None, required=False, initial=None,
       
    18                  choices=None, help=None, eidparam=False):
       
    19         self.required = required
       
    20         if widget is not None:
       
    21             self.widget = widget
       
    22         if isinstance(self.widget, type):
       
    23             self.widget = self.widget()
       
    24         self.name = name
       
    25         self.label = label or name
       
    26         self.id = id or name
       
    27         self.initial = initial
       
    28         self.choices = choices
       
    29         self.help = help
       
    30         self.eidparam = eidparam
       
    31         self.role = 'subject'
       
    32         # global fields ordering in forms
       
    33         self.creation_rank = Field.creation_rank
       
    34         Field.creation_rank += 1
       
    35 
       
    36     def __unicode__(self):
       
    37         return u'<%s name=%r label=%r id=%r initial=%r>' % (
       
    38             self.__class__.__name__, self.name, self.label,
       
    39             self.id, self.initial)
       
    40 
       
    41     def __repr__(self):
       
    42         return self.__unicode__().encode('utf-8')
       
    43 
       
    44     def set_name(self, name):
       
    45         assert name
       
    46         self.name = name
       
    47         if not self.id:
       
    48             self.id = name
       
    49         if not self.label:
       
    50             self.label = name
       
    51             
       
    52     def is_visible(self):
       
    53         return not isinstance(self.widget, HiddenInput)
       
    54     
       
    55     def actual_fields(self, form):
       
    56         yield self
       
    57     
       
    58     def format_value(self, req, value):
       
    59         if isinstance(value, (list, tuple)):
       
    60             return [self.format_single_value(req, val) for val in value]
       
    61         return self.format_single_value(req, value)
       
    62     
       
    63     def format_single_value(self, req, value):
       
    64         if value is None:
       
    65             return u''
       
    66         return unicode(value)
       
    67 
       
    68     def get_widget(self, form):
       
    69         return self.widget
       
    70     
       
    71     def example_format(self, req):
       
    72         return u''
       
    73 
       
    74     def render(self, form, renderer):
       
    75         return self.get_widget(form).render(form, self)
       
    76 
       
    77     def vocabulary(self, form):
       
    78         if self.choices is not None:
       
    79             return self.choices
       
    80         return form.form_field_vocabulary(self)
       
    81 
       
    82     
       
    83 class StringField(Field):
       
    84     def __init__(self, max_length=None, **kwargs):
       
    85         super(StringField, self).__init__(**kwargs)
       
    86         self.max_length = max_length
       
    87 
       
    88 
       
    89 class TextField(Field):
       
    90     widget = TextArea
       
    91     def __init__(self, rows=10, cols=80, **kwargs):
       
    92         super(TextField, self).__init__(**kwargs)
       
    93         self.rows = rows
       
    94         self.cols = cols
       
    95 
       
    96 
       
    97 class RichTextField(TextField):
       
    98     widget = None
       
    99     def __init__(self, format_field=None, **kwargs):
       
   100         super(RichTextField, self).__init__(**kwargs)
       
   101         self.format_field = format_field
       
   102 
       
   103     def get_widget(self, form):
       
   104         if self.widget is None:
       
   105             if self.use_fckeditor(form):
       
   106                 return FCKEditor()
       
   107             return TextArea()
       
   108         return self.widget
       
   109 
       
   110     def get_format_field(self, form):
       
   111         if self.format_field:
       
   112             return self.format_field
       
   113         # we have to cache generated field since it's use as key in the
       
   114         # context dictionnary
       
   115         req = form.req
       
   116         try:
       
   117             return req.data[self]
       
   118         except KeyError:
       
   119             if self.use_fckeditor(form):
       
   120                 # if fckeditor is used and format field isn't explicitly
       
   121                 # deactivated, we want an hidden field for the format
       
   122                 widget = HiddenInput()
       
   123                 choices = None
       
   124             else:
       
   125                 # else we want a format selector
       
   126                 # XXX compute vocabulary
       
   127                 widget = Select
       
   128                 choices = [(req._(format), format) for format in FormatConstraint().vocabulary(req=req)]
       
   129             field = StringField(name=self.name + '_format', widget=widget,
       
   130                                 choices=choices)
       
   131             req.data[self] = field
       
   132             return field
       
   133     
       
   134     def actual_fields(self, form):
       
   135         yield self
       
   136         format_field = self.get_format_field(form)
       
   137         if format_field:
       
   138             yield format_field
       
   139             
       
   140     def use_fckeditor(self, form):
       
   141         """return True if fckeditor should be used to edit entity's attribute named
       
   142         `attr`, according to user preferences
       
   143         """
       
   144         if form.req.use_fckeditor():
       
   145             return form.form_field_format(self) == 'text/html'
       
   146         return False
       
   147 
       
   148     def render(self, form, renderer):
       
   149         format_field = self.get_format_field(form)
       
   150         if format_field:
       
   151             result = format_field.render(form, renderer)
       
   152         else:
       
   153             result = u''
       
   154         return result + self.get_widget(form).render(form, self)
       
   155 
       
   156     
       
   157 class FileField(StringField):
       
   158     widget = FileInput
       
   159     needs_multipart = True
       
   160     
       
   161     def __init__(self, format_field=None, encoding_field=None, **kwargs):
       
   162         super(FileField, self).__init__(**kwargs)
       
   163         self.format_field = format_field
       
   164         self.encoding_field = encoding_field
       
   165         
       
   166     def actual_fields(self, form):
       
   167         yield self
       
   168         if self.format_field:
       
   169             yield self.format_field
       
   170         if self.encoding_field:
       
   171             yield self.encoding_field
       
   172 
       
   173     def render(self, form, renderer):
       
   174         wdgs = [self.get_widget(form).render(form, self)]
       
   175         if self.format_field or self.encoding_field:
       
   176             divid = '%s-advanced' % form.context[self]['name']
       
   177             wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' %
       
   178                         (html_escape(toggle_action(divid)),
       
   179                          form.req._('show advanced fields'),
       
   180                          html_escape(form.req.build_url('data/puce_down.png')),
       
   181                          form.req._('show advanced fields')))
       
   182             wdgs.append(u'<div id="%s" class="hidden">' % divid)
       
   183             if self.format_field:
       
   184                 wdgs.append(self.render_subfield(form, self.format_field, renderer))
       
   185             if self.encoding_field:
       
   186                 wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
       
   187             wdgs.append(u'</div>')            
       
   188         if not self.required and form.context[self]['value']:
       
   189             # trick to be able to delete an uploaded file
       
   190             wdgs.append(u'<br/>')
       
   191             wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'],
       
   192                                    type=u'checkbox'))
       
   193             wdgs.append(form.req._('detach attached file'))
       
   194         return u'\n'.join(wdgs)
       
   195 
       
   196     def render_subfield(self, form, field, renderer):
       
   197         return (renderer.render_label(form, field)
       
   198                 + field.render(form, renderer)
       
   199                 + renderer.render_help(form, field)
       
   200                 + u'<br/>')
       
   201 
       
   202         
       
   203 class EditableFileField(FileField):
       
   204     editable_formats = ('text/plain', 'text/html', 'text/rest')
       
   205     
       
   206     def render(self, form, renderer):
       
   207         wdgs = [super(EditableFileField, self).render(form, renderer)]
       
   208         if form.form_field_format(self) in self.editable_formats:
       
   209             data = form.form_field_value(self, {}, load_bytes=True)
       
   210             if data:
       
   211                 encoding = form.form_field_encoding(self)
       
   212                 try:
       
   213                     form.context[self]['value'] = unicode(data.getvalue(), encoding)
       
   214                 except UnicodeError:
       
   215                     pass
       
   216                 else:
       
   217                     if not self.required:
       
   218                         msg = form.req._(
       
   219                             'You can either submit a new file using the browse button above'
       
   220                             ', or choose to remove already uploaded file by checking the '
       
   221                             '"detach attached file" check-box, or edit file content online '
       
   222                             'with the widget below.')
       
   223                     else:
       
   224                         msg = form.req._(
       
   225                             'You can either submit a new file using the browse button above'
       
   226                             ', or edit file content online with the widget below.')
       
   227                     wdgs.append(u'<p><b>%s</b></p>' % msg)
       
   228                     wdgs.append(TextArea(setdomid=False).render(form, self))
       
   229                     # XXX restore form context?
       
   230         return '\n'.join(wdgs)
       
   231 
       
   232         
       
   233 class IntField(Field):
       
   234     def __init__(self, min=None, max=None, **kwargs):
       
   235         super(IntField, self).__init__(**kwargs)
       
   236         self.min = min
       
   237         self.max = max
       
   238 
       
   239 
       
   240 class BooleanField(Field):
       
   241     widget = Radio
       
   242         
       
   243     def vocabulary(self, form):
       
   244         if self.choices:
       
   245             return self.choices
       
   246         return [(form.req._('yes'), '1'), (form.req._('no'), '')]
       
   247 
       
   248 
       
   249 class FloatField(IntField):    
       
   250     def format_single_value(self, req, value):
       
   251         formatstr = entity.req.property_value('ui.float-format')
       
   252         if value is None:
       
   253             return u''
       
   254         return formatstr % float(value)
       
   255 
       
   256     def render_example(self, req):
       
   257         return self.format_value(req, 1.234)
       
   258 
       
   259 
       
   260 class DateField(StringField):
       
   261     format_prop = 'ui.date-format'
       
   262     widget = DateTimePicker
       
   263     
       
   264     def format_single_value(self, req, value):
       
   265         return value and ustrftime(value, req.property_value(self.format_prop)) or u''
       
   266 
       
   267     def render_example(self, req):
       
   268         return self.format_value(req, datetime.now())
       
   269 
       
   270 
       
   271 class DateTimeField(DateField):
       
   272     format_prop = 'ui.datetime-format'
       
   273 
       
   274 
       
   275 class TimeField(DateField):
       
   276     format_prop = 'ui.datetime-format'
       
   277 
       
   278 
       
   279 class HiddenInitialValueField(Field):
       
   280     def __init__(self, visible_field, name):
       
   281         super(HiddenInitialValueField, self).__init__(name=name,
       
   282                                                       widget=HiddenInput,
       
   283                                                       eidparam=True)
       
   284         self.visible_field = visible_field
       
   285     
       
   286                  
       
   287 class RelationField(Field):
       
   288     def __init__(self, **kwargs):
       
   289         super(RelationField, self).__init__(**kwargs)
       
   290 
       
   291     @staticmethod
       
   292     def fromcardinality(card, role, **kwargs):
       
   293         return RelationField(widget=Select(multiple=card in '*+'),
       
   294                              **kwargs)
       
   295         
       
   296     def vocabulary(self, form):
       
   297         entity = form.entity
       
   298         req = entity.req
       
   299         # first see if its specified by __linkto form parameters
       
   300         linkedto = entity.linked_to(self.name, self.role)
       
   301         if linkedto:
       
   302             entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto)
       
   303             return [(entity.view('combobox'), entity.eid) for entity in entities]
       
   304         # it isn't, check if the entity provides a method to get correct values
       
   305         res = []
       
   306         if not self.required:
       
   307             res.append(('', INTERNAL_FIELD_VALUE))
       
   308         # vocabulary doesn't include current values, add them
       
   309         if entity.has_eid():
       
   310             rset = entity.related(self.name, self.role)
       
   311             relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()]
       
   312         else:
       
   313             relatedvocab = []
       
   314         return res + form.form_field_vocabulary(self) + relatedvocab
       
   315     
       
   316     def format_single_value(self, req, value):
       
   317         return value
       
   318 
       
   319 
       
   320 def stringfield_from_constraints(constraints, **kwargs):
       
   321     field = None
       
   322     for cstr in constraints:
       
   323         if isinstance(cstr, StaticVocabularyConstraint):
       
   324             return StringField(widget=Select(vocabulary=cstr.vocabulary),
       
   325                                **kwargs)
       
   326         if isinstance(cstr, SizeConstraint) and cstr.max is not None:
       
   327             if cstr.max > 257:
       
   328                 field = textfield_from_constraint(cstr, **kwargs)
       
   329             else:
       
   330                 field = StringField(max_length=cstr.max, **kwargs)
       
   331     return field or TextField(**kwargs)
       
   332         
       
   333 
       
   334 def textfield_from_constraint(constraint, **kwargs):
       
   335     if 256 < constraint.max < 513:
       
   336         rows, cols = 5, 60
       
   337     else:
       
   338         rows, cols = 10, 80
       
   339     return TextField(rows, cols, **kwargs)
       
   340 
       
   341 
       
   342 def find_field(eclass, subjschema, rschema, role='subject'):
       
   343     """return the most adapated widget to edit the relation
       
   344     'subjschema rschema objschema' according to information found in the schema
       
   345     """
       
   346     fieldclass = None
       
   347     if role == 'subject':
       
   348         objschema = rschema.objects(subjschema)[0]
       
   349         cardidx = 0
       
   350     else:
       
   351         objschema = rschema.subjects(subjschema)[0]
       
   352         cardidx = 1
       
   353     card = rschema.rproperty(subjschema, objschema, 'cardinality')[cardidx]
       
   354     required = card in '1+'
       
   355     if rschema in eclass.widgets:
       
   356         fieldclass = eclass.widgets[rschema]
       
   357         if isinstance(fieldclass, basestring):
       
   358             return StringField(name=rschema.type)
       
   359     elif not rschema.is_final():
       
   360         return RelationField.fromcardinality(card, role,name=rschema.type,
       
   361                                              required=required)
       
   362     else:
       
   363         fieldclass = FIELDS[objschema]
       
   364     if fieldclass is StringField:
       
   365         constraints = rschema.rproperty(subjschema, objschema, 'constraints')
       
   366         return stringfield_from_constraints(constraints, name=rschema.type,
       
   367                                             required=required)
       
   368     return fieldclass(name=rschema.type, required=required)
       
   369 
       
   370 FIELDS = {
       
   371     'Boolean':  BooleanField,
       
   372     'Bytes':    FileField,
       
   373     'Date':     DateField,
       
   374     'Datetime': DateTimeField,
       
   375     'Int':      IntField,
       
   376     'Float':    FloatField,
       
   377     'Decimal':  StringField,
       
   378     'Password': StringField,
       
   379     'String' :  StringField,
       
   380     'Time':     TimeField,
       
   381     }
       
   382 views/