--- a/web/form.py Thu Mar 26 18:58:14 2009 +0100
+++ b/web/form.py Thu Mar 26 18:59:01 2009 +0100
@@ -8,36 +8,24 @@
from warnings import warn
-from simplejson import dumps
-
from logilab.common.compat import any
from logilab.common.decorators import iclassmethod
-from logilab.mtconverter import html_escape
-from cubicweb import typed_eid
-from cubicweb.appobject import AppObject
-from cubicweb.selectors import yes, non_final_entity
+from cubicweb.appobject import AppRsetObject
+from cubicweb.selectors import yes, non_final_entity, match_kwargs, one_line_rset
from cubicweb.view import NOINDEX, NOFOLLOW
from cubicweb.common import tags
from cubicweb.web import INTERNAL_FIELD_VALUE, eid_param, stdmsgs
from cubicweb.web.httpcache import NoHTTPCacheManager
-from cubicweb.web.controller import NAV_FORM_PARAMETERS, redirect_params
+from cubicweb.web.controller import NAV_FORM_PARAMETERS
from cubicweb.web.formfields import (Field, StringField, RelationField,
HiddenInitialValueField)
-from cubicweb.web.formwidgets import HiddenInput
+from cubicweb.web.formrenderers import FormRenderer
+from cubicweb.web import formwidgets as fwdgs
-def relation_id(eid, rtype, target, reid):
- if target == 'subject':
- return u'%s:%s:%s' % (eid, rtype, reid)
- return u'%s:%s:%s' % (reid, rtype, eid)
-
-def toggable_relation_link(eid, nodeid, label='x'):
- js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid)))
- return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label)
-
-
+# XXX should disappear
class FormMixIn(object):
"""abstract form mix-in
XXX: you should inherit from this FIRST (obscure pb with super call)"""
@@ -49,8 +37,8 @@
add_to_breadcrumbs = False
skip_relations = set()
- def __init__(self, req, rset):
- super(FormMixIn, self).__init__(req, rset)
+ def __init__(self, req, rset, **kwargs):
+ super(FormMixIn, self).__init__(req, rset, **kwargs)
self.maxrelitems = self.req.property_value('navigation.related-limit')
self.maxcomboitems = self.req.property_value('navigation.combobox-limit')
self.force_display = not not req.form.get('__force_display')
@@ -110,7 +98,42 @@
if inlined_entity.get_widget(irschema, x).need_multipart:
return True
return False
+
+ def button(self, label, klass='validateButton', tabindex=None, **kwargs):
+ if tabindex is None:
+ tabindex = self.req.next_tabindex()
+ return tags.input(value=label, klass=klass, **kwargs)
+
+ def action_button(self, label, onclick=None, __action=None, **kwargs):
+ if onclick is None:
+ onclick = "postForm('__action_%s', \'%s\', \'%s\')" % (
+ __action, label, self.domid)
+ return self.button(label, onclick=onclick, **kwargs)
+
+ def button_ok(self, label=None, type='submit', name='defaultsubmit',
+ **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
+ return self.button(label, name=name, type=type, **kwargs)
+
+ def button_apply(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
+ return self.action_button(label, __action='apply', type=type, **kwargs)
+
+ def button_delete(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
+ return self.action_button(label, __action='delete', type=type, **kwargs)
+
+ def button_cancel(self, label=None, type='button', **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+ return self.action_button(label, __action='cancel', type=type, **kwargs)
+
+ def button_reset(self, label=None, type='reset', name='__action_cancel',
+ **kwargs):
+ label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
+ return self.button(label, type=type, **kwargs)
+
+ # XXX deprecated with new form system
def error_message(self):
"""return formatted error message
@@ -137,113 +160,6 @@
errormsg = '<ul>%s</ul>' % errormsg
return u'<div class="errorMessage">%s</div>' % errormsg
return u''
-
- def restore_pending_inserts(self, entity, cell=False):
- """used to restore edition page as it was before clicking on
- 'search for <some entity type>'
-
- """
- eid = entity.eid
- cell = cell and "div_insert_" or "tr"
- pending_inserts = set(self.req.get_pending_inserts(eid))
- for pendingid in pending_inserts:
- eidfrom, rtype, eidto = pendingid.split(':')
- if typed_eid(eidfrom) == entity.eid: # subject
- label = display_name(self.req, rtype, 'subject')
- reid = eidto
- else:
- label = display_name(self.req, rtype, 'object')
- reid = eidfrom
- jscall = "javascript: cancelPendingInsert('%s', '%s', null, %s);" \
- % (pendingid, cell, eid)
- rset = self.req.eid_rset(reid)
- eview = self.view('text', rset, row=0)
- # XXX find a clean way to handle baskets
- if rset.description[0][0] == 'Basket':
- eview = '%s (%s)' % (eview, display_name(self.req, 'Basket'))
- yield rtype, pendingid, jscall, label, reid, eview
-
-
- def force_display_link(self):
- return (u'<span class="invisible">'
- u'[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]'
- u'</span>' % self.req._('view all'))
-
- def relations_table(self, entity):
- """yiels 3-tuples (rtype, target, related_list)
- where <related_list> itself a list of :
- - node_id (will be the entity element's DOM id)
- - appropriate javascript's togglePendingDelete() function call
- - status 'pendingdelete' or ''
- - oneline view of related entity
- """
- eid = entity.eid
- pending_deletes = self.req.get_pending_deletes(eid)
- # XXX (adim) : quick fix to get Folder relations
- for label, rschema, target in entity.srelations_by_category(('generic', 'metadata'), 'add'):
- if rschema in self.skip_relations:
- continue
- relatedrset = entity.related(rschema, target, limit=self.limit)
- toggable_rel_link = self.toggable_relation_link_func(rschema)
- related = []
- for row in xrange(relatedrset.rowcount):
- nodeid = relation_id(eid, rschema, target, relatedrset[row][0])
- if nodeid in pending_deletes:
- status = u'pendingDelete'
- label = '+'
- else:
- status = u''
- label = 'x'
- dellink = toggable_rel_link(eid, nodeid, label)
- eview = self.view('oneline', relatedrset, row=row)
- related.append((nodeid, dellink, status, eview))
- yield (rschema, target, related)
-
- def toggable_relation_link_func(self, rschema):
- if not rschema.has_perm(self.req, 'delete'):
- return lambda x, y, z: u''
- return toggable_relation_link
-
-
- def redirect_url(self, entity=None):
- """return a url to use as next direction if there are some information
- specified in current form params, else return the result the reset_url
- method which should be defined in concrete classes
- """
- rparams = redirect_params(self.req.form)
- if rparams:
- return self.build_url('view', **rparams)
- return self.reset_url(entity)
-
- def reset_url(self, entity):
- raise NotImplementedError('implement me in concrete classes')
-
- BUTTON_STR = u'<input class="validateButton" type="submit" name="%s" value="%s" tabindex="%s"/>'
- ACTION_SUBMIT_STR = u'<input class="validateButton" type="button" onclick="postForm(\'%s\', \'%s\', \'%s\')" value="%s" tabindex="%s"/>'
-
- def button_ok(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_OK).capitalize()
- return self.BUTTON_STR % ('defaultsubmit', label, tabindex or 2)
-
- def button_apply(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_APPLY).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_apply', label, self.domid,
- label, tabindex or 3)
-
- def button_delete(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_DELETE).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_delete', label, self.domid,
- label, tabindex or 3)
-
- def button_cancel(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
- return self.ACTION_SUBMIT_STR % ('__action_cancel', label, self.domid,
- label, tabindex or 4)
-
- def button_reset(self, label=None, tabindex=None):
- label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize()
- return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % (
- label, tabindex or 4)
###############################################################################
@@ -264,38 +180,43 @@
return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict)
-class FieldsForm(FormMixIn, AppObject):
+class FieldsForm(FormMixIn, AppRsetObject):
__metaclass__ = metafieldsform
__registry__ = 'forms'
__select__ = yes()
+
+ is_subform = False
+
+ # attributes overrideable through __init__
internal_fields = ('__errorurl',) + NAV_FORM_PARAMETERS
needs_js = ('cubicweb.edition.js',)
needs_css = ('cubicweb.form.css',)
-
- def __init__(self, req, rset=None, domid=None, title=None, action='edit',
- onsubmit="return freezeFormButtons('%(domid)s');",
- cssclass=None, cssstyle=None, cwtarget=None, buttons=None,
- redirect_path=None, set_error_url=True, copy_nav_params=False):
- self.req = req
- self.rset = rset
- self.config = req.vreg.config
- self.domid = domid or 'form'
- self.title = title
- self.action = action
- self.onsubmit = onsubmit
- self.cssclass = cssclass
- self.cssstyle = cssstyle
- self.cwtarget = cwtarget
- self.redirect_path = redirect_path
+ domid = 'form'
+ title = None
+ action = None
+ onsubmit = "return freezeFormButtons('%(domid)s');"
+ cssclass = None
+ cssstyle = None
+ cwtarget = None
+ buttons = None
+ redirect_path = None
+ set_error_url = True
+ copy_nav_params = False
+
+ def __init__(self, req, rset=None, row=None, col=None, **kwargs):
+ super(FieldsForm, self).__init__(req, rset, row=row, col=col)
+ self.buttons = kwargs.pop('buttons', [])
+ for key, val in kwargs.items():
+ assert hasattr(self.__class__, key) and not key[0] == '_', key
+ setattr(self, key, val)
self.fields = list(self.__class__._fields_)
- if set_error_url:
+ if self.set_error_url:
self.form_add_hidden('__errorurl', req.url())
- if copy_nav_params:
+ if self.copy_nav_params:
for param in NAV_FORM_PARAMETERS:
- value = req.form.get(param)
+ value = kwargs.get(param, req.form.get(param))
if value:
self.form_add_hidden(param, initial=value)
- self.buttons = buttons or []
self.context = None
@iclassmethod
@@ -309,14 +230,24 @@
return field
raise Exception('field %s not found' % name)
+ @iclassmethod
+ def remove_field(cls_or_self, field):
+ if isinstance(cls_or_self, type):
+ fields = cls_or_self._fields_
+ else:
+ fields = cls_or_self.fields
+ fields.remove(field)
+
@property
def form_needs_multipart(self):
return any(field.needs_multipart for field in self.fields)
def form_add_hidden(self, name, value=None, **kwargs):
- self.fields.append(StringField(name=name, widget=HiddenInput,
- initial=value, **kwargs))
-
+ field = StringField(name=name, widget=fwdgs.HiddenInput, initial=value,
+ **kwargs)
+ self.fields.append(field)
+ return field
+
def add_media(self):
"""adds media (CSS & JS) required by this widget"""
if self.needs_js:
@@ -357,6 +288,14 @@
else:
value = field.initial
return value
+
+ def form_field_error(self, field):
+ """return validation error for widget's field, if any"""
+ errex = self.req.data.get('formerrors')
+ if errex and field.name in errex.errors:
+ self.req.data['displayederrors'].add(field.name)
+ return u'<span class="error">%s</span>' % errex.errors[field.name]
+ return u''
def form_field_format(self, field):
return self.req.property_value('ui.default-text-format')
@@ -378,19 +317,28 @@
class EntityFieldsForm(FieldsForm):
- __select__ = non_final_entity()
+ __select__ = (match_kwargs('entity') | (one_line_rset & non_final_entity()))
internal_fields = FieldsForm.internal_fields + ('__type', 'eid')
+ domid = 'entityForm'
def __init__(self, *args, **kwargs):
- kwargs.setdefault('domid', 'entityForm')
- self.entity = kwargs.pop('entity', None)
+ self.edited_entity = kwargs.pop('entity', None)
+ msg = kwargs.pop('submitmsg', None)
super(EntityFieldsForm, self).__init__(*args, **kwargs)
+ if self.edited_entity is None:
+ self.edited_entity = self.complete_entity(self.row)
self.form_add_hidden('__type', eidparam=True)
self.form_add_hidden('eid')
+ if msg is not None:
+ # If we need to directly attach the new object to another one
+ for linkto in self.req.list_form_param('__linkto'):
+ self.form_add_hidden('__linkto', linkto)
+ msg = '%s %s' % (msg, self.req._('and linked'))
+ self.form_add_hidden('__message', msg)
def form_render(self, **values):
- self.form_add_entity_hiddens(self.entity.e_schema)
+ self.form_add_entity_hiddens(self.edited_entity.e_schema)
return super(EntityFieldsForm, self).form_render(**values)
def form_add_entity_hiddens(self, eschema):
@@ -433,21 +381,24 @@
fieldname = field.name
if fieldname.startswith('edits-') or fieldname.startswith('edito-'):
# edit[s|o]- fieds must have the actual value stored on the entity
- if self.entity.has_eid():
- value = self._form_field_entity_value(field.visible_field,
- default_initial=False)
+ if hasattr(field, 'visible_field'):
+ if self.edited_entity.has_eid():
+ value = self._form_field_entity_value(field.visible_field,
+ default_initial=False)
+ else:
+ value = INTERNAL_FIELD_VALUE
else:
- value = INTERNAL_FIELD_VALUE
+ value = field.initial
elif fieldname == '__type':
- value = self.entity.id
+ value = self.edited_entity.id
elif fieldname == 'eid':
- value = self.entity.eid
+ value = self.edited_entity.eid
elif fieldname in values:
value = values[fieldname]
elif fieldname in self.req.form:
value = self.req.form[fieldname]
else:
- if self.entity.has_eid() and field.eidparam:
+ if self.edited_entity.has_eid() and field.eidparam:
# use value found on the entity or field's initial value if it's
# not an attribute of the entity (XXX may conflicts and get
# undesired value)
@@ -455,11 +406,11 @@
load_bytes=load_bytes)
else:
defaultattr = 'default_%s' % fieldname
- if hasattr(self.entity, defaultattr):
+ if hasattr(self.edited_entity, defaultattr):
# XXX bw compat, default_<field name> on the entity
warn('found %s on %s, should be set on a specific form'
- % (defaultattr, self.entity.id), DeprecationWarning)
- value = getattr(self.entity, defaultattr)
+ % (defaultattr, self.edited_entity.id), DeprecationWarning)
+ value = getattr(self.edited_entity, defaultattr)
elif hasattr(self, defaultattr):
# search for default_<field name> on the form instance
value = getattr(self, defaultattr)
@@ -471,38 +422,47 @@
return value
def form_field_format(self, field):
- entity = self.entity
+ entity = self.edited_entity
if field.eidparam and entity.e_schema.has_metadata(field.name, 'format') and (
entity.has_eid() or '%s_format' % field.name in entity):
- return self.entity.attribute_metadata(field.name, 'format')
+ return self.edited_entity.attribute_metadata(field.name, 'format')
return self.req.property_value('ui.default-text-format')
def form_field_encoding(self, field):
- entity = self.entity
+ entity = self.edited_entity
if field.eidparam and entity.e_schema.has_metadata(field.name, 'encoding') and (
entity.has_eid() or '%s_encoding' % field.name in entity):
- return self.entity.attribute_metadata(field.name, 'encoding')
+ return self.edited_entity.attribute_metadata(field.name, 'encoding')
return super(EntityFieldsForm, self).form_field_encoding(field)
+
+ def form_field_error(self, field):
+ """return validation error for widget's field, if any"""
+ errex = self.req.data.get('formerrors')
+ if errex and errex.eid == self.edited_entity.eid and field.name in errex.errors:
+ self.req.data['displayederrors'].add(field.name)
+ return u'<span class="error">%s</span>' % errex.errors[field.name]
+ return u''
def _form_field_entity_value(self, field, default_initial=True, load_bytes=False):
- attr = field.name
+ attr = field.name
+ entity = self.edited_entity
if field.role == 'object':
attr = 'reverse_' + attr
- else:
- attrtype = self.entity.e_schema.destination(attr)
+ elif entity.e_schema.subject_relation(attr).is_final():
+ attrtype = entity.e_schema.destination(attr)
if attrtype == 'Password':
- return self.entity.has_eid() and INTERNAL_FIELD_VALUE or ''
+ return entity.has_eid() and INTERNAL_FIELD_VALUE or ''
if attrtype == 'Bytes':
- if self.entity.has_eid():
+ if entity.has_eid():
if load_bytes:
- return getattr(self.entity, attr)
+ return getattr(entity, attr)
# XXX value should reflect if some file is already attached
return True
return False
if default_initial:
- value = getattr(self.entity, attr, field.initial)
+ value = getattr(entity, attr, field.initial)
else:
- value = getattr(self.entity, attr)
+ value = getattr(entity, attr)
if isinstance(field, RelationField):
# in this case, value is the list of related entities
value = [ent.eid for ent in value]
@@ -510,24 +470,24 @@
def form_field_name(self, field):
if field.eidparam:
- return eid_param(field.name, self.entity.eid)
+ return eid_param(field.name, self.edited_entity.eid)
return field.name
def form_field_id(self, field):
if field.eidparam:
- return eid_param(field.id, self.entity.eid)
+ return eid_param(field.id, self.edited_entity.eid)
return field.id
def form_field_vocabulary(self, field, limit=None):
role, rtype = field.role, field.name
try:
- vocabfunc = getattr(self.entity, '%s_%s_vocabulary' % (role, rtype))
+ vocabfunc = getattr(self.edited_entity, '%s_%s_vocabulary' % (role, rtype))
except AttributeError:
vocabfunc = getattr(self, '%s_relation_vocabulary' % role)
else:
# XXX bw compat, default_<field name> on the entity
warn('found %s_%s_vocabulary on %s, should be set on a specific form'
- % (role, rtype, self.entity.id), DeprecationWarning)
+ % (role, rtype, self.edited_entity.id), DeprecationWarning)
# NOTE: it is the responsibility of `vocabfunc` to sort the result
# (direclty through RQL or via a python sort). This is also
# important because `vocabfunc` might return a list with
@@ -544,7 +504,7 @@
"""defaut vocabulary method for the given relation, looking for
relation's object entities (i.e. self is the subject)
"""
- entity = self.entity
+ entity = self.edited_entity
if isinstance(rtype, basestring):
rtype = entity.schema.rschema(rtype)
done = None
@@ -566,7 +526,7 @@
"""defaut vocabulary method for the given relation, looking for
relation's subject entities (i.e. self is the object)
"""
- entity = self.entity
+ entity = self.edited_entity
if isinstance(rtype, basestring):
rtype = entity.schema.rschema(rtype)
done = None
@@ -587,7 +547,7 @@
limit=None, done=None):
if done is None:
done = set()
- rset = self.entity.unrelated(rtype, targettype, role, limit)
+ rset = self.edited_entity.unrelated(rtype, targettype, role, limit)
res = []
for entity in rset.entities():
if entity.eid in done:
@@ -597,118 +557,13 @@
return res
-class MultipleFieldsForm(FieldsForm):
+class CompositeForm(FieldsForm):
+ """form composed for sub-forms"""
+
def __init__(self, *args, **kwargs):
- super(MultipleFieldsForm, self).__init__(*args, **kwargs)
+ super(CompositeForm, self).__init__(*args, **kwargs)
self.forms = []
def form_add_subform(self, subform):
+ subform.is_subform = True
self.forms.append(subform)
-
-
-# form renderers ############
-
-class FormRenderer(object):
- button_bar_class = u'formButtonBar'
-
- def __init__(self, display_fields=None, display_label=True,
- display_help=True, button_bar_class=None):
- self.display_fields = display_fields # None -> all fields
- self.display_label = display_label
- self.display_help = display_help
- if button_bar_class is not None:
- self.button_bar_class = button_bar_class
-
- # renderer interface ######################################################
-
- def render(self, form, values):
- form.add_media()
- data = []
- w = data.append
- w(self.open_form(form))
- w(u'<div id="progress">%s</div>' % form.req._('validating...'))
- w(u'<fieldset>')
- w(tags.input(type='hidden', name='__form_id', value=form.domid))
- if form.redirect_path:
- w(tags.input(type='hidden', name='__redirectpath', value=form.redirect_path))
- self.render_fields(w, form, values)
- self.render_buttons(w, form)
- w(u'</fieldset>')
- w(u'</form>')
- return '\n'.join(data)
-
- def render_label(self, form, field):
- label = form.req._(field.label)
- attrs = {'for': form.context[field]['id']}
- if field.required:
- attrs['class'] = 'required'
- return tags.label(label, **attrs)
-
- def render_help(self, form, field):
- help = [ u'<br/>' ]
- descr = field.help
- if descr:
- help.append('<span class="helper">%s</span>' % form.req._(descr))
- example = field.example_format(form.req)
- if example:
- help.append('<span class="helper">(%s: %s)</span>'
- % (form.req._('sample format'), example))
- return u' '.join(help)
-
- # specific methods (mostly to ease overriding) #############################
-
- def open_form(self, form):
- if form.form_needs_multipart:
- enctype = 'multipart/form-data'
- else:
- enctype = 'application/x-www-form-urlencoded'
- tag = ('<form action="%s" method="post" id="%s" enctype="%s"' % (
- html_escape(form.action or '#'), form.domid, enctype))
- if form.onsubmit:
- tag += ' onsubmit="%s"' % html_escape(form.onsubmit % form.__dict__)
- if form.cssstyle:
- tag += ' style="%s"' % html_escape(form.cssstyle)
- if form.cssclass:
- tag += ' class="%s"' % html_escape(form.cssclass)
- if form.cwtarget:
- tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
- return tag + '>'
-
- def display_field(self, form, field):
- return (self.display_fields is None
- or field.name in self.display_fields
- or field.name in form.internal_fields)
-
- def render_fields(self, w, form, values):
- form.form_build_context(values)
- fields = form.fields[:]
- for field in form.fields:
- if not self.display_field(form, field):
- fields.remove(field)
-
- if not field.is_visible():
- w(field.render(form, self))
- fields.remove(field)
- if fields:
- self._render_fields(fields, w, form)
- for childform in getattr(form, 'forms', []):
- self.render_fields(w, childform, values)
-
- def _render_fields(self, fields, w, form,):
- w(u'<table>')
- for field in fields:
- w(u'<tr>')
- if self.display_label:
- w(u'<th>%s</th>' % self.render_label(form, field))
- w(u'<td style="width:100%;">')
- w(field.render(form, self))
- if self.display_help:
- w(self.render_help(form, field))
- w(u'</td></tr>')
- w(u'</table>')
-
- def render_buttons(self, w, form):
- w(u'<table class="%s">\n<tr>\n' % self.button_bar_class)
- for button in form.form_buttons():
- w(u'<td>%s</td>\n' % button)
- w(u'</tr></table>')