refactor form field value handling, to get a nicer api and an easier algorithm to get field's value
Details:
* new .typed_value / .display_value on fields
* droped form_field_value on form
* .value attribute of field instead of .initial
* nicer field's __init__, allowing to give a lambda as value's value
--- a/web/formfields.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/formfields.py Mon Dec 21 19:45:24 2009 +0100
@@ -39,6 +39,7 @@
result += sorted(partresult)
return result
+_MARKER = object()
class Field(object):
"""field class is introduced to control what's displayed in forms. It makes
@@ -64,8 +65,8 @@
class which may be overriden per instance.
:required:
bool flag telling if the field is required or not.
- :initial:
- initial value, used when no value specified by other means.
+ :value:
+ field's value, used when no value specified by other means. XXX explain
:choices:
static vocabulary for this field. May be a list of values or a list of
(label, value) tuples if specified.
@@ -95,32 +96,38 @@
# class attribute used for ordering of fields in a form
__creation_rank = 0
- def __init__(self, name=None, id=None, label=None, help=None,
- widget=None, required=False, initial=None,
- choices=None, sort=True, internationalizable=False,
- eidparam=False, role='subject', fieldset=None, order=None):
+ eidparam = False
+ role = None
+ id = None
+ help = None
+ required = False
+ choices = None
+ sort = True
+ internationalizable = False
+ fieldset = None
+ order = None
+ value = _MARKER
+
+ def __init__(self, name=None, label=None, widget=None, **kwargs):
+ for key, val in kwargs.items():
+ if key == 'initial':
+ warn('[3.6] use value instead of initial', DeprecationWarning,
+ stacklevel=3)
+ key = 'value'
+ assert hasattr(self.__class__, key) and not key[0] == '_', key
+ setattr(self, key, val)
self.name = name
- self.id = id or name
self.label = label or name
- self.help = help
- self.required = required
- self.initial = initial
- self.choices = choices
- self.sort = sort
- self.internationalizable = internationalizable
- self.eidparam = eidparam
- self.role = role
- self.fieldset = fieldset
+ # has to be done after other attributes initialization
self.init_widget(widget)
- self.order = order
# ordering number for this field instance
self.creation_rank = Field.__creation_rank
Field.__creation_rank += 1
def __unicode__(self):
- return u'<%s name=%r label=%r id=%r initial=%r visible=%r @%x>' % (
- self.__class__.__name__, self.name, self.label,
- self.id, self.initial, self.is_visible(), id(self))
+ return u'<%s name=%r eidparam=%s role=%r id=%r value=%r visible=%r @%x>' % (
+ self.__class__.__name__, self.name, self.eidparam, self.role,
+ self.id, self.value, self.is_visible(), id(self))
def __repr__(self):
return self.__unicode__().encode('utf-8')
@@ -134,11 +141,9 @@
self.widget = self.widget()
def set_name(self, name):
- """automatically set .id and .label when name is set"""
+ """automatically set .label when name is set"""
assert name
self.name = name
- if not self.id:
- self.id = name
if not self.label:
self.label = name
@@ -200,6 +205,62 @@
return eid_param(id, form.edited_entity.eid)
return id
+ def display_value(self, form):
+ """return field's *string* value to use for display
+
+ looks in
+ 1. previously submitted form values if any (eg on validation error)
+ 2. req.form
+ 3. extra form args given to render_form
+ 4. field's typed value
+
+ values found in 1. and 2. are expected te be already some 'display'
+ value while those found in 3. and 4. are expected to be correctly typed.
+ """
+ qname = self.input_name(form)
+ if qname in form.form_previous_values:
+ return form.form_previous_values[qname]
+ if qname in form._cw.form:
+ return form._cw.form[qname]
+ if self.name != qname and self.name in form._cw.form:
+ return form._cw.form[self.name]
+ for key in (self, qname):
+ try:
+ value = form.formvalues[key]
+ break
+ except:
+ continue
+ else:
+ if self.name != qname and self.name in form.formvalues:
+ value = form.formvalues[self.name]
+ else:
+ value = self.typed_value(form)
+ if value != INTERNAL_FIELD_VALUE:
+ value = self.format_value(form._cw, value)
+ return value
+
+ def typed_value(self, form, load_bytes=False):
+ if self.value is not _MARKER:
+ if callable(self.value):
+ return self.value(form)
+ return self.value
+ return self._typed_value(form, load_bytes)
+
+ def _typed_value(self, form, load_bytes=False):
+ if self.eidparam:
+ assert form._cw.vreg.schema.rschema(self.name).final
+ entity = form.edited_entity
+ if entity.has_eid() or self.name in entity:
+ return getattr(entity, self.name)
+ formattr = '%s_%s_default' % (self.role, self.name)
+ if hasattr(form, formattr):
+ warn('[3.6] %s.%s deprecated, use field.value' % (
+ form.__class__.__name__, formattr), DeprecationWarning)
+ return getattr(form, formattr)()
+ if self.eidparam:
+ return entity.e_schema.default(self.name)
+ return None
+
def example_format(self, req):
"""return a sample string describing what can be given as input for this
field
@@ -292,6 +353,18 @@
widget.attrs.setdefault('rows', 5)
+class PasswordField(StringField):
+ widget = PasswordInput
+
+ def _typed_value(self, form, load_bytes=False):
+ if self.eidparam:
+ # no way to fetch actual password value with cw
+ if form.edited_entity.has_eid():
+ return INTERNAL_FIELD_VALUE
+ return form.edited_entity.e_schema.default(self.name)
+ return super(PasswordField, self)._typed_value(form, load_bytes)
+
+
class RichTextField(StringField):
widget = None
def __init__(self, format_field=None, **kwargs):
@@ -324,14 +397,14 @@
# if fckeditor is used and format field isn't explicitly
# deactivated, we want an hidden field for the format
fkwargs['widget'] = HiddenInput()
- fkwargs['initial'] = 'text/html'
+ fkwargs['value'] = 'text/html'
else:
# else we want a format selector
fkwargs['widget'] = Select()
fcstr = FormatConstraint()
fkwargs['choices'] = fcstr.vocabulary(form=form)
fkwargs['internationalizable'] = True
- fkwargs['initial'] = lambda f: f.form_field_format(self)
+ fkwargs['value'] = self.format
fkwargs['eidparam'] = self.eidparam
field = StringField(name=self.name + '_format', **fkwargs)
req.data[self] = field
@@ -383,6 +456,19 @@
if self.name_field:
yield self.name_field
+ def _typed_value(self, form, load_bytes=False):
+ if self.eidparam:
+ if form.edited_entity.has_eid():
+ if load_bytes:
+ return getattr(form.edited_entity, self.name)
+ # don't actually load data
+ # XXX value should reflect if some file is already attached
+ # * try to display name metadata
+ # * check length(data) / data != null
+ return True
+ return False
+ return super(FileField, self)._typed_value(form, load_bytes)
+
def render(self, form, renderer):
wdgs = [self.get_widget(form).render(form, self, renderer)]
if self.format_field or self.encoding_field:
@@ -400,7 +486,7 @@
if self.encoding_field:
wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
wdgs.append(u'</div>')
- if not self.required and form.context[self]['value']:
+ if not self.required and self.display_value(form):
# trick to be able to delete an uploaded file
wdgs.append(u'<br/>')
wdgs.append(tags.input(name=self.input_name(form, u'__detach'),
@@ -439,12 +525,12 @@
def render(self, form, renderer):
wdgs = [super(EditableFileField, self).render(form, renderer)]
- if form.form_field_format(self) in self.editable_formats:
- data = form.form_field_value(self, load_bytes=True)
+ if self.format(form) in self.editable_formats:
+ data = self.typed_value(form, load_bytes=True)
if data:
encoding = form.form_field_encoding(self)
try:
- form.context[self]['value'] = unicode(data.getvalue(), encoding)
+ form.formvalues[self] = unicode(data.getvalue(), encoding)
except UnicodeError:
pass
else:
@@ -592,6 +678,25 @@
vocab = vocab_sort(vocab)
return vocab
+ def form_init(self, form):
+ if not self.display_value(form):
+ value = form.edited_entity.linked_to(self.name, self.role)
+ if value:
+ searchedvalues = ['%s:%s:%s' % (self.name, eid, self.role)
+ for eid in value]
+ # remove associated __linkto hidden fields
+ for field in form.root_form.fields_by_name('__linkto'):
+ if field.value in searchedvalues:
+ form.root_form.remove_field(field)
+ form.formvalues[self] = value
+
+ def _typed_value(self, form, load_bytes=False):
+ entity = form.edited_entity
+ # non final relation field
+ if entity.has_eid() or entity.relation_cached(self.name, self.role):
+ return [r[0] for r in entity.related(self.name, self.role)]
+ return ()
+
def format_single_value(self, req, value):
return value
@@ -628,9 +733,6 @@
if rschema.final:
if rdef.get('internationalizable'):
kwargs.setdefault('internationalizable', True)
- def get_default(form, es=eschema, rs=rschema):
- return es.default(rs)
- kwargs.setdefault('initial', get_default)
else:
targetschema = rdef.subject
card = rdef.role_cardinality(role)
@@ -647,10 +749,6 @@
return None
fieldclass = FIELDS[targetschema]
if fieldclass is StringField:
- if targetschema == 'Password':
- # special case for Password field: specific PasswordInput widget
- kwargs.setdefault('widget', PasswordInput())
- return StringField(**kwargs)
if eschema.has_metadata(rschema, 'format'):
# use RichTextField instead of StringField if the attribute has
# a "format" metadata. But getting information from constraints
@@ -687,7 +785,7 @@
'Int': IntField,
'Float': FloatField,
'Decimal': StringField,
- 'Password': StringField,
+ 'Password': PasswordField,
'String' : StringField,
'Time': TimeField,
}
--- a/web/formwidgets.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/formwidgets.py Mon Dec 21 19:45:24 2009 +0100
@@ -345,7 +345,7 @@
return txtwidget + cal_button
def _render_calendar_popup(self, form, field):
- value = form.form_field_value(field)
+ value = field.typed_value(form)
if not value:
value = date.today()
inputid = field.dom_id(form)
--- a/web/views/cwproperties.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/views/cwproperties.py Mon Dec 21 19:45:24 2009 +0100
@@ -335,7 +335,7 @@
self.widget = NotEditableWidget(entity.printable_value('value'), msg)
# XXX race condition when used from CWPropertyForm, should not rely on
# instance attributes
- self.initial = pdef['default']
+ self.value = pdef['default']
self.help = pdef['help']
vocab = pdef['vocabulary']
if vocab is not None:
--- a/web/views/forms.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/views/forms.py Mon Dec 21 19:45:24 2009 +0100
@@ -12,6 +12,7 @@
from logilab.common.compat import any
from logilab.common.deprecation import deprecated
+from cubicweb import typed_eid
from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param
from cubicweb.web import form, formwidgets as fwdgs
@@ -113,7 +114,7 @@
def form_add_hidden(self, name, value=None, **kwargs):
"""add an hidden field to the form"""
kwargs.setdefault('widget', fwdgs.HiddenInput)
- field = StringField(name=name, initial=value, **kwargs)
+ field = StringField(name=name, value=value, **kwargs)
if 'id' in kwargs:
# by default, hidden input don't set id attribute. If one is
# explicitly specified, ensure it will be set
@@ -142,77 +143,21 @@
self._cw, rset=self.cw_rset,
row=self.cw_row, col=self.cw_col)
- def build_context(self, rendervalues=None):
+ formvalues = None
+ def build_context(self, formvalues=None):
"""build form context values (the .context attribute which is a
dictionary with field instance as key associated to a dictionary
containing field 'name' (qualified), 'id', 'value' (for display, always
a string).
-
- rendervalues is an optional dictionary containing extra form values
- given to render()
"""
- if self.context is not None:
+ if self.formvalues is not None:
return # already built
- self.context = context = {}
- # ensure rendervalues is a dict
- if rendervalues is None:
- rendervalues = {}
+ self.formvalues = formvalues or {}
# use a copy in case fields are modified while context is build (eg
# __linkto handling for instance)
for field in self.fields[:]:
for field in field.actual_fields(self):
field.form_init(self)
- value = self.form_field_display_value(field, rendervalues)
- context[field] = {'value': value,
- 'name': self.form_field_name(field),
- 'id': self.form_field_id(field),
- }
-
- def form_field_display_value(self, field, rendervalues, load_bytes=False):
- """return field's *string* value to use for display
-
- looks in
- 1. previously submitted form values if any (eg on validation error)
- 2. req.form
- 3. extra kw args given to render_form
- 4. field's typed value
-
- values found in 1. and 2. are expected te be already some 'display'
- value while those found in 3. and 4. are expected to be correctly typed.
- """
- value = self._req_display_value(field)
- if value is None:
- if field.name in rendervalues:
- value = rendervalues[field.name]
- elif field.name in self.cw_extra_kwargs:
- value = self.cw_extra_kwargs[field.name]
- else:
- value = self.form_field_value(field, load_bytes)
- if callable(value):
- value = value(self)
- if value != INTERNAL_FIELD_VALUE:
- value = field.format_value(self._cw, value)
- return value
-
- def _req_display_value(self, field):
- qname = self.form_field_name(field)
- if qname in self.form_previous_values:
- return self.form_previous_values[qname]
- if qname in self._cw.form:
- return self._cw.form[qname]
- if field.name in self._cw.form:
- return self._cw.form[field.name]
- return None
-
- def form_field_value(self, field, load_bytes=False):
- """return field's *typed* value"""
- myattr = '%s_%s_default' % (field.role, field.name)
- if hasattr(self, myattr):
- return getattr(self, myattr)()
- value = field.initial
- if callable(value):
- value = value(self)
- return value
def form_field_error(self, field):
"""return validation error for widget's field, if any"""
@@ -297,6 +242,15 @@
return '%s#%s' % (self.edited_entity.absolute_url(), self.domid)
return '%s#%s' % (self.req.url(), self.domid)
+ def build_context(self, formvalues=None):
+ super(EntityFieldsForm, self).build_context(formvalues)
+ edited = set()
+ for field in self.fields:
+ if field.eidparam:
+ edited.add(field.role_name())
+ self.add_hidden('_cw_edited_fields', u','.join(edited),
+ eidparam=True)
+
def _field_has_error(self, field):
"""return true if the field has some error in given validation exception
"""
@@ -353,43 +307,7 @@
self.form_renderer_id, self._cw, rset=self.cw_rset, row=self.cw_row,
col=self.cw_col, entity=self.edited_entity)
- def form_field_value(self, field, load_bytes=False):
- """return field's *typed* value
- overriden to deal with
- * special eid / __type
- * lookup for values on edited entities
- """
- attr = field.name
- entity = self.edited_entity
- if attr == 'eid':
- return entity.eid
- if not field.eidparam:
- return super(EntityFieldsForm, self).form_field_value(field, load_bytes)
- if attr == '__type':
- return entity.__regid__
- if self._cw.vreg.schema.rschema(attr).final:
- attrtype = entity.e_schema.destination(attr)
- if attrtype == 'Password':
- return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
- if attrtype == 'Bytes':
- if entity.has_eid():
- if load_bytes:
- return getattr(entity, attr)
- # XXX value should reflect if some file is already attached
- return True
- return False
- if entity.has_eid() or attr in entity:
- value = getattr(entity, attr)
- else:
- value = self._form_field_default_value(field, load_bytes)
- return value
- # non final relation field
- if entity.has_eid() or entity.relation_cached(attr, field.role):
- value = [r[0] for r in entity.related(attr, field.role)]
- else:
- value = self._form_field_default_value(field, load_bytes)
- return value
def form_field_format(self, field):
"""return MIME type used for the given (text or bytes) field"""
@@ -406,28 +324,21 @@
entity.has_eid() or '%s_encoding' % field.name in entity):
return self.edited_entity.attr_metadata(field.name, 'encoding')
return super(EntityFieldsForm, self).form_field_encoding(field)
-
- def form_field_name(self, field):
- """return qualified name for the given field"""
- if field.eidparam:
- return eid_param(field.name, self.edited_entity.eid)
- return field.name
-
- def form_field_id(self, field):
- """return dom id for the given field"""
- if field.eidparam:
- return eid_param(field.id, self.edited_entity.eid)
- return field.id
-
# XXX all this vocabulary handling should be on the field, no?
def form_field_vocabulary(self, field, limit=None):
"""return vocabulary for the given field"""
role, rtype = field.role, field.name
method = '%s_%s_vocabulary' % (role, rtype)
+ def actual_eid(self, eid):
+ # should be either an int (existant entity) or a variable (to be
+ # created entity)
+ assert eid or eid == 0, repr(eid) # 0 is a valid eid
try:
vocabfunc = getattr(self, method)
except AttributeError:
+ return typed_eid(eid)
+ except ValueError:
try:
# XXX bw compat, <role>_<rtype>_vocabulary on the entity
vocabfunc = getattr(self.edited_entity, method)
@@ -511,6 +422,10 @@
if limit is not None and len(result) >= limit:
break
return result
+ return self._cw.data['eidmap'][eid]
+ except KeyError:
+ self._cw.data['eidmap'][eid] = None
+ return None
def editable_relations(self):
return ()
@@ -533,10 +448,10 @@
subform.parent_form = self
self.forms.append(subform)
- def build_context(self, rendervalues=None):
- super(CompositeFormMixIn, self).build_context(rendervalues)
+ def build_context(self, formvalues=None):
+ super(CompositeFormMixIn, self).build_context(formvalues)
for form in self.forms:
- form.build_context(rendervalues)
+ form.build_context(formvalues)
class CompositeForm(CompositeFormMixIn, FieldsForm):
--- a/web/views/massmailing.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/views/massmailing.py Mon Dec 21 19:45:24 2009 +0100
@@ -39,8 +39,11 @@
__regid__ = 'massmailing'
sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
- label=_('From:'))
- recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'))
+ label=_('From:'),
+ value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
+ recipient = ff.StringField(widget=CheckBox(), label=_('Recipients:'),
+ choices=recipient_vocabulary,
+ value= lambda f: [entity.eid for entity in f.cw_rset.entities() if entity.get_email()])
subject = ff.StringField(label=_('Subject:'), max_length=256)
mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField',
inputid='mailbody'))
@@ -57,12 +60,10 @@
return [(label, value) for label, value in vocab if label]
return super(MassMailingForm, self).form_field_vocabulary(field)
- def form_field_value(self, field, values):
- if field.name == 'recipient':
- return [entity.eid for entity in self.cw_rset.entities() if entity.get_email()]
- elif field.name == 'mailbody':
- field.widget.attrs['cubicweb:variables'] = ','.join(self.get_allowed_substitutions())
- return super(MassMailingForm, self).form_field_value(field, values)
+ def __init__(self, *args, **kwargs):
+ super(MassMailingForm, self).__init__(*args, **kwargs)
+ field = self.field_by_name('mailbody')
+ field.widget.attrs['cubicweb:variables'] = ','.join(self.get_allowed_substitutions())
def get_allowed_substitutions(self):
attrs = []
@@ -126,5 +127,5 @@
req.add_css('cubicweb.mailform.css')
from_addr = '%s <%s>' % (req.user.dc_title(), req.user.get_email())
form = self._cw.vreg['forms'].select('massmailing', self._cw, rset=self.cw_rset,
- action='sendmail', domid='sendmail')
- self.w(form.render(formvalues=dict(sender=from_addr)))
+ action='sendmail', domid='sendmail')
+ self.w(form.render())
--- a/web/views/sparql.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/views/sparql.py Mon Dec 21 19:45:24 2009 +0100
@@ -28,7 +28,7 @@
resultvid = formfields.StringField(choices=((_('table'), 'table'),
(_('sparql xml'), 'sparqlxml')),
widget=fwdgs.Radio,
- initial='table')
+ value='table')
form_buttons = [fwdgs.SubmitButton()]
@property
def action(self):
--- a/web/views/workflow.py Mon Dec 21 19:25:07 2009 +0100
+++ b/web/views/workflow.py Mon Dec 21 19:45:24 2009 +0100
@@ -86,8 +86,11 @@
trinfo = self._cw.vreg['etypes'].etype_class('TrInfo')(self._cw)
trinfo.eid = self._cw.varmaker.next()
subform = self._cw.vreg['forms'].select('edition', self._cw, entity=trinfo,
- mainform=False)
- subform.field_by_name('by_transition').widget = fwdgs.HiddenInput()
+ mainform=False)
+ subform.field_by_name('wf_info_for', 'subject').value = entity.eid
+ trfield = subform.field_by_name('by_transition', 'subject')
+ trfield.widget = fwdgs.HiddenInput()
+ trfield.value = transition.eid
form.add_subform(subform)
return form