--- 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>')
--- a/web/formfields.py Thu Mar 26 18:58:14 2009 +0100
+++ b/web/formfields.py Thu Mar 26 18:59:01 2009 +0100
@@ -307,7 +307,7 @@
return RelationField(widget=Select(multiple=card in '*+'), **kwargs)
def vocabulary(self, form):
- entity = form.entity
+ entity = form.edited_entity
req = entity.req
# first see if its specified by __linkto form parameters
linkedto = entity.linked_to(self.name, self.role)
@@ -334,8 +334,8 @@
field = None
for cstr in constraints:
if isinstance(cstr, StaticVocabularyConstraint):
- return StringField(widget=Select(vocabulary=cstr.vocabulary),
- **kwargs)
+ kwargs.setdefault('widget', Select(vocabulary=cstr.vocabulary))
+ return StringField(**kwargs)
if isinstance(cstr, SizeConstraint) and cstr.max is not None:
if cstr.max > 257:
rows_cols_from_constraint(cstr, kwargs)
@@ -375,7 +375,8 @@
if fieldclass is StringField:
if targetschema == 'Password':
# special case for Password field: specific PasswordInput widget
- return StringField(widget=PasswordInput(), **kwargs)
+ 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
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/web/formrenderers.py Thu Mar 26 18:59:01 2009 +0100
@@ -0,0 +1,399 @@
+"""form renderers, responsible to layout a form to html
+
+:organization: Logilab
+:copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
+:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
+"""
+__docformat__ = "restructuredtext en"
+
+from logilab.common import dictattr
+from logilab.mtconverter import html_escape
+
+from simplejson import dumps
+
+from cubicweb.common import tags
+from cubicweb.web import eid_param
+from cubicweb.web import formwidgets as fwdgs
+from cubicweb.web.widgets import checkbox
+
+class FormRenderer(object):
+ """basic renderer displaying fields in a two columns table label | value
+ """
+ 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, values))
+ w(u'<div id="progress">%s</div>' % form.req._('validating...'))
+ w(u'<fieldset>')
+ w(tags.input(type=u'hidden', name=u'__form_id',
+ value=values.get('formvid', form.id)))
+ 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>')
+ errormsg = self.error_message(form)
+ if errormsg:
+ data.insert(0, errormsg)
+ 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 error_message(self, form):
+ """return formatted error message
+
+ This method should be called once inlined field errors has been consumed
+ """
+ req = form.req
+ errex = req.data.get('formerrors')
+ # get extra errors
+ if errex is not None:
+ errormsg = req._('please correct the following errors:')
+ displayed = req.data['displayederrors']
+ errors = sorted((field, err) for field, err in errex.errors.items()
+ if not field in displayed)
+ if errors:
+ if len(errors) > 1:
+ templstr = '<li>%s</li>\n'
+ else:
+ templstr = ' %s\n'
+ for field, err in errors:
+ if field is None:
+ errormsg += templstr % err
+ else:
+ errormsg += templstr % '%s: %s' % (req._(field), err)
+ if len(errors) > 1:
+ errormsg = '<ul>%s</ul>' % errormsg
+ return u'<div class="errorMessage">%s</div>' % errormsg
+ return u''
+
+ def open_form(self, form, values):
+ if form.form_needs_multipart:
+ enctype = 'multipart/form-data'
+ else:
+ enctype = 'application/x-www-form-urlencoded'
+ if form.action is None:
+ action = form.req.build_url('edit')
+ else:
+ action = form.action
+ tag = ('<form action="%s" method="post" id="%s" enctype="%s"' % (
+ html_escape(action or '#'), form.domid, enctype))
+ if form.onsubmit:
+ tag += ' onsubmit="%s"' % html_escape(form.onsubmit % dictattr(form))
+ 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 = self._render_hidden_fields(w, form)
+ if fields:
+ self._render_fields(fields, w, form, values)
+ self.render_child_forms(w, form, values)
+
+ def render_child_forms(self, w, form, values):
+ # render
+ for childform in getattr(form, 'forms', []):
+ self.render_fields(w, childform, values)
+
+ def _render_hidden_fields(self, w, form):
+ fields = form.fields[:]
+ for field in form.fields:
+ if not self.display_field(form, field):
+ fields.remove(field)
+ elif not field.is_visible():
+ w(field.render(form, self))
+ fields.remove(field)
+ return fields
+
+ def _render_fields(self, fields, w, form, values):
+ w(u'<table id="%s" class="attributeForm" style="width:100%%;">')
+ for field in fields:
+ w(u'<tr>')
+ if self.display_label:
+ w(u'<th class="labelCol">%s</th>' % self.render_label(form, field))
+ error = form.form_field_error(field)
+ if error:
+ w(u'<td class="error" style="width:100%;">')
+ w(error)
+ else:
+ 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>')
+
+
+
+class EntityCompositeFormRenderer(FormRenderer):
+ """specific renderer for multiple entities edition form (muledit)"""
+ def render_fields(self, w, form, values):
+ if not form.is_subform:
+ w(u'<table class="listing">')
+ super(EntityCompositeFormRenderer, self).render_fields(w, form, values)
+ if not form.is_subform:
+ w(u'</table>')
+
+ def _render_fields(self, fields, w, form, values):
+ if form.is_subform:
+ entity = form.edited_entity
+ values = form.req.data.get('formvalues', ())
+ qeid = eid_param('eid', entity.eid)
+ cbsetstate = "setCheckboxesState2('eid', %s, 'checked')" % html_escape(dumps(entity.eid))
+ w(u'<tr class="%s">' % (entity.row % 2 and u'even' or u'odd'))
+ # XXX turn this into a widget used on the eid field
+ w(u'<td>%s</td>' % checkbox('eid', entity.eid, checked=qeid in values))
+ for field in fields:
+ error = form.form_field_error(field)
+ if error:
+ w(u'<td class="error">')
+ w(error)
+ else:
+ w(u'<td>')
+ if isinstance(field.widget, (fwdgs.Select, fwdgs.CheckBox, fwdgs.Radio)):
+ field.widget.attrs['onchange'] = cbsetstate
+ elif isinstance(field.widget, fwdgs.Input):
+ field.widget.attrs['onkeypress'] = cbsetstate
+ w(u'<div>%s</div>' % field.render(form, self))
+ w(u'/<td>')
+ else:
+ # main form, display table headers
+ w(u'<tr class="header">')
+ w(u'<th align="left">%s</th>'
+ % tags.input(type='checkbox', title=form.req._('toggle check boxes'),
+ onclick="setCheckboxesState('eid', this.checked)"))
+ for field in self.forms[0].fields:
+ if self.display_field(form, field) and field.is_visible():
+ w(u'<th>%s</th>' % form.req._(field.label))
+ w(u'</tr>')
+
+
+
+class EntityFormRenderer(FormRenderer):
+ """specific renderer for entity edition form (edition)"""
+
+ def open_form(self, form, values):
+ attrs_fs_label = ('<div class="iformTitle"><span>%s</span></div>'
+ % form.req._('main informations'))
+ attrs_fs_label += '<div class="formBody">'
+ return super(EntityFormRenderer, self).open_form(form, values) + attrs_fs_label
+
+ def render_fields(self, w, form, values):
+ super(EntityFormRenderer, self).render_fields(w, form, values)
+ self.inline_entities_form(w, form)
+ if form.edited_entity.has_eid():
+ self.relations_form(w, form)
+ w(u'</div>') # close extra div introducted by open_form
+
+ def _render_fields(self, fields, w, form, values):
+ if not form.edited_entity.has_eid() or form.edited_entity.has_perm('update'):
+ super(EntityFormRenderer, self)._render_fields(fields, w, form, values)
+
+ def render_buttons(self, w, form):
+ buttons = form.form_buttons()
+ if len(buttons) == 3:
+ w("""<table width="100%%">
+ <tbody>
+ <tr><td align="center">
+ %s
+ </td><td style="align: right; width: 50%%;">
+ %s
+ %s
+ </td></tr>
+ </tbody>
+ </table>""" % tuple(buttons))
+ else:
+ super(EntityFormRenderer, self).render_buttons(w, form)
+
+ def relations_form(self, w, form):
+ srels_by_cat = form.srelations_by_category(('generic', 'metadata'), 'add')
+ if not srels_by_cat:
+ return u''
+ req = form.req
+ _ = req._
+ label = u'%s :' % _('This %s' % form.edited_entity.e_schema).capitalize()
+ eid = form.edited_entity.eid
+ w(u'<fieldset class="subentity">')
+ w(u'<legend class="iformTitle">%s</legend>' % label)
+ w(u'<table id="relatedEntities">')
+ for rschema, target, related in form.relations_table():
+ # already linked entities
+ if related:
+ w(u'<tr><th class="labelCol">%s</th>' % rschema.display_name(req, target))
+ w(u'<td>')
+ w(u'<ul>')
+ for viewparams in related:
+ w(u'<li class="invisible">%s<div id="span%s" class="%s">%s</div></li>'
+ % (viewparams[1], viewparams[0], viewparams[2], viewparams[3]))
+ if not form.force_display and form.maxrelitems < len(related):
+ link = (u'<span class="invisible">'
+ '[<a href="javascript: window.location.href+=\'&__force_display=1\'">%s</a>]'
+ '</span>' % form.req._('view all'))
+ w(u'<li class="invisible">%s</li>' % link)
+ w(u'</ul>')
+ w(u'</td>')
+ w(u'</tr>')
+ pendings = list(form.restore_pending_inserts())
+ if not pendings:
+ w(u'<tr><th> </th><td> </td></tr>')
+ else:
+ for row in pendings:
+ # soon to be linked to entities
+ w(u'<tr id="tr%s">' % row[1])
+ w(u'<th>%s</th>' % row[3])
+ w(u'<td>')
+ w(u'<a class="handle" title="%s" href="%s">[x]</a>' %
+ (_('cancel this insert'), row[2]))
+ w(u'<a id="a%s" class="editionPending" href="%s">%s</a>'
+ % (row[1], row[4], html_escape(row[5])))
+ w(u'</td>')
+ w(u'</tr>')
+ w(u'<tr id="relationSelectorRow_%s" class="separator">' % eid)
+ w(u'<th class="labelCol">')
+ w(u'<span>%s</span>' % _('add relation'))
+ w(u'<select id="relationSelector_%s" tabindex="%s" onchange="javascript:showMatchingSelect(this.options[this.selectedIndex].value,%s);">'
+ % (eid, req.next_tabindex(), html_escape(dumps(eid))))
+ w(u'<option value="">%s</option>' % _('select a relation'))
+ for i18nrtype, rschema, target in srels_by_cat:
+ # more entities to link to
+ w(u'<option value="%s_%s">%s</option>' % (rschema, target, i18nrtype))
+ w(u'</select>')
+ w(u'</th>')
+ w(u'<td id="unrelatedDivs_%s"></td>' % eid)
+ w(u'</tr>')
+ w(u'</table>')
+ w(u'</fieldset>')
+
+ def inline_entities_form(self, w, form):
+ """create a form to edit entity's inlined relations"""
+ entity = form.edited_entity
+ __ = form.req.__
+ for rschema, targettypes, role in form.relations_by_category('inlineview', 'add'):
+ # show inline forms only if there's one possible target type
+ # for rschema
+ if len(targettypes) != 1:
+ self.warning('entity related by the %s relation should have '
+ 'inlined form but there is multiple target types, '
+ 'dunno what to do', rschema)
+ continue
+ targettype = targettypes[0].type
+ if form.should_inline_relation_form(rschema, targettype, role):
+ w(u'<div id="inline%sslot">' % rschema)
+ existant = entity.has_eid() and entity.related(rschema)
+ if existant:
+ # display inline-edition view for all existing related entities
+ w(self.view('inline-edition', existant, rtype=rschema, role=role,
+ ptype=entity.e_schema, peid=entity.eid,
+ **kwargs))
+ if role == 'subject':
+ card = rschema.rproperty(entity.e_schema, targettype, 'cardinality')[0]
+ else:
+ card = rschema.rproperty(targettype, entity.e_schema, 'cardinality')[1]
+ # there is no related entity and we need at least one: we need to
+ # display one explicit inline-creation view
+ if form.should_display_inline_creation_form(rschema, existant, card):
+ w(self.view('inline-creation', None, etype=targettype,
+ peid=entity.eid, ptype=entity.e_schema,
+ rtype=rschema, role=role, **kwargs))
+ # we can create more than one related entity, we thus display a link
+ # to add new related entities
+ if form.should_display_add_new_relation_link(rschema, existant, card):
+ divid = "addNew%s%s%s:%s" % (targettype, rschema, role, entity.eid)
+ w(u'<div class="inlinedform" id="%s" cubicweb:limit="true">'
+ % divid)
+ js = "addInlineCreationForm('%s', '%s', '%s', '%s', '%s')" % (
+ entity.eid, entity.e_schema, targettype, rschema, role)
+ if card in '1?':
+ js = "toggleVisibility('%s'); %s" % (divid, js)
+ w(u'<a class="addEntity" id="add%s:%slink" href="javascript: %s" >+ %s.</a>'
+ % (rschema, entity.eid, js, __('add a %s' % targettype)))
+ w(u'</div>')
+ w(u'<div class="trame_grise"> </div>')
+ w(u'</div>')
+
+
+class EntityInlinedFormRenderer(EntityFormRenderer):
+ """specific renderer for entity inlined edition form
+ (inline-[creation|edition])
+ """
+ def render(self, form, values):
+ form.add_media()
+ data = []
+ w = data.append
+ try:
+ w(u'<div id="div-%(divid)s" onclick="%(divonclick)s">' % values)
+ except KeyError:
+ w(u'<div id="div-%(divid)s">' % values)
+ else:
+ w(u'<div id="notice-%s" class="notice">%s</div>' % (
+ values['divid'], form.req._('click on the box to cancel the deletion')))
+ w(u'<div class="iformBody">')
+ values['removemsg'] = form.req.__('remove this %s' % form.edited_entity.e_schema)
+ w(u'<div class="iformTitle"><span>%(title)s</span> '
+ '#<span class="icounter">1</span> '
+ '[<a href="javascript: %(removejs)s;noop();">%(removemsg)s</a>]</div>'
+ % values)
+ self.render_fields(w, form, values)
+ w(u'</div>')
+ return '\n'.join(data)
+
+ def render_fields(self, w, form, values):
+ form.form_build_context(values)
+ w(u'<fieldset id="fs-%(divid)s">' % values)
+ fields = self._render_hidden_fields(w, form)
+ w(u'</fieldset>')
+ w(u'<fieldset class="subentity">')
+ if fields:
+ self._render_fields(fields, w, form, values)
+ self.render_child_forms(w, form, values)
+ self.inline_entities_form(w, form)
+ w(u'</fieldset>')
+
--- a/web/formwidgets.py Thu Mar 26 18:58:14 2009 +0100
+++ b/web/formwidgets.py Thu Mar 26 18:59:01 2009 +0100
@@ -206,8 +206,6 @@
if not value:
value = date.today()
year, month = value.year, value.month
- onclick = "toggleCalendar('%s', '%s', %s, %s);" % (
- helperid, inputid, year, month)
return (u"""<a onclick="toggleCalendar('%s', '%s', %s, %s);" class="calhelper">
<img src="%s" title="%s" alt="" /></a><div class="calpopup hidden" id="%s"></div>"""
% (helperid, inputid, year, month,
@@ -226,5 +224,5 @@
def render(self, form, field):
self.add_media(form)
- name, values, attrs = self._render_attrs(form, field)
+ attrs = self._render_attrs(form, field)[-1]
return tags.div(**attrs)
--- a/web/test/unittest_form.py Thu Mar 26 18:58:14 2009 +0100
+++ b/web/test/unittest_form.py Thu Mar 26 18:59:01 2009 +0100
@@ -25,31 +25,56 @@
creation_date = DateTimeField(widget=DateTimePicker)
form = CustomChangeStateForm(self.req, redirect_path='perdu.com',
entity=self.entity)
- self.assertTextEquals(form.form_render(state=123, trcomment=u''),
- ''' ''')
+ form.form_render(state=123, trcomment=u'')
def test_change_state_form(self):
form = ChangeStateForm(self.req, redirect_path='perdu.com',
entity=self.entity)
- self.assertTextEquals(form.form_render(state=123, trcomment=u''),
- ''' ''')
+ form.form_render(state=123, trcomment=u'')
def test_delete_conf_form_multi(self):
rset = self.execute('EGroup X')
- self.assertTextEquals(self.view('deleteconf', rset, template=None).source,
- '')
+ self.view('deleteconf', rset, template=None).source
def test_massmailing_form(self):
self.execute('INSERT EmailAddress X: X address L + "@cubicweb.org", '
'U use_email X WHERE U is EUser, U login L')
rset = self.execute('EUser X')
- self.assertTextEquals(self.view('massmailing', rset, template=None).source,
- '')
+ self.view('massmailing', rset, template=None)
+
+ def test_automatic_edition_form(self):
+ rset = self.execute('EUser X')
+ self.view('edition', rset, row=0, template=None).source
+
+ def test_automatic_edition_form(self):
+ rset = self.execute('EUser X')
+ self.view('copy', rset, row=0, template=None).source
+
+ def test_automatic_creation_form(self):
+ self.view('creation', None, etype='EUser', template=None).source
+
+ def test_automatic_muledit_form(self):
+ rset = self.execute('EUser X')
+ self.view('muledit', rset, template=None).source
+
+ def test_automatic_reledit_form(self):
+ rset = self.execute('EUser X')
+ self.view('reledit', rset, row=0, rtype='login', template=None).source
+
+ def test_automatic_inline_edit_form(self):
+ geid = self.execute('EGroup X LIMIT 1')[0][0]
+ rset = self.execute('EUser X LIMIT 1')
+ self.view('inline-edition', rset, row=0, rtype='in_group', peid=geid, template=None).source
+
+ def test_automatic_inline_creation_form(self):
+ geid = self.execute('EGroup X LIMIT 1')[0][0]
+ self.view('inline-creation', None, etype='EUser', rtype='in_group', peid=geid, template=None).source
+
# fields tests ############################################################
def _render_entity_field(self, name, form):
- form.form_add_entity_hiddens(form.entity.e_schema)
+ form.form_add_entity_hiddens(form.edited_entity.e_schema)
form.form_build_context({})
return form.field_by_name(name).render(form, self.renderer)
--- a/web/views/editforms.py Thu Mar 26 18:58:14 2009 +0100
+++ b/web/views/editforms.py Thu Mar 26 18:59:01 2009 +0100
@@ -7,20 +7,43 @@
"""
__docformat__ = "restructuredtext en"
+from copy import copy
+
from simplejson import dumps
-from cubicweb.selectors import match_kwargs, one_line_rset, non_final_entity
+from logilab.mtconverter import html_escape
+
+from cubicweb import typed_eid
+from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
+ specified_etype_implements, yes)
+from cubicweb.rtags import RelationTags
from cubicweb.utils import make_uid
from cubicweb.view import EntityView
from cubicweb.common import tags
-from cubicweb.web import stdmsgs
-from cubicweb.web.form import MultipleFieldsForm, EntityFieldsForm, FormMixIn, FormRenderer
+from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, formwidgets
+from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormMixIn
from cubicweb.web.formfields import guess_field
+from cubicweb.web.formrenderers import (FormRenderer, EntityFormRenderer,
+ EntityCompositeFormRenderer,
+ EntityInlinedFormRenderer)
+_ = unicode
-_ = unicode
+def relation_id(eid, rtype, role, reid):
+ """return an identifier for a relation between two entities"""
+ if role == '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'):
+ """return javascript snippet to delete/undelete a relation between two
+ entities
+ """
+ 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)
class DeleteConfForm(EntityView):
+ """form used to confirm deletion of some entities"""
id = 'deleteconf'
title = _('delete')
domid = 'deleteconf'
@@ -37,19 +60,18 @@
% _('this action is not reversible!'))
# XXX above message should have style of a warning
w(u'<h4>%s</h4>\n' % _('Do you want to delete the following element(s) ?'))
- form = MultipleFieldsForm(req, domid='deleteconf', action=self.build_url('edit'),
+ form = CompositeForm(req, domid='deleteconf', action=self.build_url('edit'),
onsubmit=self.onsubmit, copy_nav_params=True)
+ # XXX tabindex
form.buttons.append(form.button_delete(label=stdmsgs.YES))
form.buttons.append(form.button_cancel(label=stdmsgs.NO))
done = set()
w(u'<ul>\n')
- for i in xrange(self.rset.rowcount):
- if self.rset[i][0] in done:
+ for entity in self.rset.entities():
+ if entity.eid in done:
continue
- done.add(self.rset[i][0])
- entity = self.rset.get_entity(i, 0)
- subform = EntityFieldsForm(req, set_error_url=False,
- entity=entity)
+ done.add(entity.eid)
+ subform = EntityFieldsForm(req, entity=entity, set_error_url=False)
form.form_add_subform(subform)
# don't use outofcontext view or any other that may contain inline edition form
w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
@@ -59,12 +81,21 @@
class ClickAndEditForm(FormMixIn, EntityView):
+ """form used to permit ajax edition of an attribute of an entity in a view
+
+ (double-click on the field to see an appropriate edition widget)
+ """
id = 'reledit'
__select__ = non_final_entity() & match_kwargs('rtype')
-
+
# FIXME editableField class could be toggleable from userprefs
-
+
+ onsubmit = ("return inlineValidateForm('%(divid)s-form', '%(rtype)s', "
+ "'%(eid)s', '%(divid)s', %(reload)s);")
+ ondblclick = "showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
+
def cell_call(self, row, col, rtype=None, role='subject', reload=False):
+ """display field to edit entity's `rtype` relation on double-click"""
entity = self.entity(row, col)
if getattr(entity, rtype) is None:
value = self.req._('not specified')
@@ -78,84 +109,520 @@
edit_key = make_uid('%s-%s' % (rtype, eid))
divid = 'd%s' % edit_key
reload = dumps(reload)
- buttons = [tags.input(klass="validateButton", type="submit", name="__action_apply",
- value=self.req._(stdmsgs.BUTTON_OK), tabindex=self.req.next_tabindex()),
+ # XXX tab index
+ buttons = [tags.input(klass="validateButton", type="submit",
+ name="__action_apply",
+ value=self.req._(stdmsgs.BUTTON_OK),
+ tabindex=self.req.next_tabindex()),
tags.input(klass="validateButton", type="button",
value=self.req._(stdmsgs.BUTTON_CANCEL),
onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (eid, rtype, divid),
tabindex=self.req.next_tabindex())]
- form = self.vreg.select_object('forms', 'edition', self.req, self.rset, row=row, col=col,
- entity=entity, domid='%s-form' % divid, action='#',
- cssstyle='display: none', buttons=buttons,
- onsubmit="return inlineValidateForm('%(divid)s-form', '%(rtype)s', '%(eid)s', '%(divid)s', %(reload)s);" % locals())
+ form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+ row=row, col=col, buttons=buttons,
+ domid='%s-form' % divid, action='#',
+ cssstyle='display: none',
+ onsubmit=self.onsubmit % locals())
renderer = FormRenderer(display_label=False, display_help=False,
display_fields=(rtype,), button_bar_class='buttonbar')
self.w(tags.div(value, klass='editableField', id=divid,
- ondblclick="showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')" % locals()))
- self.w(form.render(renderer=renderer))
+ ondblclick=self.ondblclick % locals()))
+ self.w(form.form_render(renderer=renderer))
class AutomaticEntityForm(EntityFieldsForm):
+ """base automatic form to edit any entity
+
+ Designed to be flly generated from schema but highly configurable through:
+ * rtags (rcategories, rwidgets, inlined, rpermissions)
+ * various standard form parameters
+
+ You can also easily customise it by adding/removing fields in
+ AutomaticEntityForm instances.
+ """
id = 'edition'
+
needs_js = EntityFieldsForm.needs_js + ('cubicweb.ajax.js',)
+ cwtarget = 'eformframe'
+ cssclass = 'entityForm'
+ copy_nav_params = True
+ attrcategories = ('primary', 'secondary')
+
+ # relations'category (eg primary/secondary/generic/metadata/generated)
+ rcategories = RelationTags()
+ # use primary and not generated for eid since it has to be an hidden
+ rcategories.set_rtag('primary', 'subject', 'eid')
+ rcategories.set_rtag('metadata', 'subject', 'creation_date')
+ rcategories.set_rtag('metadata', 'subject', 'modification_date')
+ rcategories.set_rtag('generated', 'subject', 'has_text')
+
+ # relations'widget (eg one of available class name in cubicweb.web.formwidgets)
+ rwidgets = RelationTags()
+ # inlined view flag for non final relations
+ inlined = RelationTags()
+ # set of tags of the form <action>_on_new on relations. <action> is a
+ # schema action (add/update/delete/read), and when such a tag is found
+ # permissions checking is by-passed and supposed to be ok
+ rpermissions_overrides = RelationTags(use_set=True)
+
+ @classmethod
+ def registered(cls, registry):
+ """build class using descriptor at registration time"""
+ super(AutomaticEntityForm, cls).registered(registry)
+ cls.init_rtags_category()
+ return cls
+
+ @classmethod
+ def init_rtags_category(cls):
+ """set default category tags for relations where it's not yet defined in
+ the category relation tags
+ """
+ for eschema in cls.schema.entities():
+ for rschema, tschemas, role in eschema.relation_definitions(True):
+ for tschema in tschemas:
+ if role == 'subject':
+ X, Y = eschema, tschema
+ card = rschema.rproperty(X, Y, 'cardinality')[0]
+ composed = rschema.rproperty(X, Y, 'composite') == 'object'
+ else:
+ X, Y = tschema, eschema
+ card = rschema.rproperty(X, Y, 'cardinality')[1]
+ composed = rschema.rproperty(X, Y, 'composite') == 'subject'
+ if not cls.rcategories.rtag(role, rschema, X, Y):
+ if card in '1+':
+ if not rschema.is_final() and composed:
+ category = 'generated'
+ else:
+ category = 'primary'
+ elif rschema.is_final():
+ category = 'secondary'
+ else:
+ category = 'generic'
+ cls.rcategories.set_rtag(category, role, rschema, X, Y)
+
def __init__(self, *args, **kwargs):
super(AutomaticEntityForm, self).__init__(*args, **kwargs)
- self.entity.complete()
- for rschema, target in self.editable_attributes(self.entity):
- field = guess_field(self.entity.__class__, self.entity.e_schema,
- rschema, target)
+ if self.edited_entity.has_eid():
+ self.edited_entity.complete()
+ for rschema, role in self.editable_attributes():
+ wdgname = self.rwidgets.etype_rtag(self.edited_entity.id, role, rschema)
+ if wdgname:
+ field = guess_field(self.edited_entity.__class__, rschema, role,
+ eidparam=True, widget=getattr(formwidgets, wdgname))
+ else:
+ field = guess_field(self.edited_entity.__class__, rschema, role,
+ eidparam=True)
self.fields.append(field)
-
+
+ def action(self):
+ """return the form's action attribute"""
+ try:
+ return self._action
+ except AttributeError:
+ return self.build_url('validateform')
+
+ def set_action(self, value):
+ self._action = value
+
+ action = property(action, set_action)
+
def form_buttons(self):
+ """return the form's buttons (as string)"""
return [self.button_ok(tabindex=self.req.next_tabindex()),
self.button_apply(tabindex=self.req.next_tabindex()),
self.button_cancel(tabindex=self.req.next_tabindex())]
- def editable_attributes(self, entity):
- # XXX both (add, delete) required for non final relations
- return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
+ def editable_attributes(self):
+ """return a list of (relation schema, role) to edit for the entity
+ """
+ return [(rschema, x) for rschema, _, x in self.relations_by_category(self.attrcategories, 'add')
if rschema != 'eid']
-class _EditionForm(EntityView):
- """primary entity edition form
+ def relations_by_category(self, categories=None, permission=None):
+ """return a list of (relation schema, target schemas, role) matching
+ categories and permission
+ """
+ if categories is not None:
+ if not isinstance(categories, (list, tuple, set, frozenset)):
+ categories = (categories,)
+ if not isinstance(categories, (set, frozenset)):
+ categories = frozenset(categories)
+ eschema = self.edited_entity.e_schema
+ rtags = self.rcategories
+ permsoverrides = self.rpermissions_overrides
+ if self.edited_entity.has_eid():
+ eid = self.edited_entity.eid
+ else:
+ eid = None
+ for rschema, targetschemas, role in eschema.relation_definitions(True):
+ if rschema in ('identity', 'has_text'):
+ continue
+ # check category first, potentially lower cost than checking
+ # permission which may imply rql queries
+ if categories is not None:
+ targetschemas = [tschema for tschema in targetschemas
+ if rtags.etype_rtag(eschema, role, rschema, tschema) in categories]
+ if not targetschemas:
+ continue
+ if permission is not None:
+ # tag allowing to hijack the permission machinery when
+ # permission is not verifiable until the entity is actually
+ # created...
+ if eid is None and '%s_on_new' % permission in permsoverrides.etype_rtags(eschema, role, rschema):
+ yield (rschema, targetschemas, role)
+ continue
+ if rschema.is_final():
+ if not rschema.has_perm(self.req, permission, eid):
+ continue
+ elif role == 'subject':
+ if not ((eid is None and rschema.has_local_role(permission)) or
+ rschema.has_perm(self.req, permission, fromeid=eid)):
+ continue
+ # on relation with cardinality 1 or ?, we need delete perm as well
+ # if the relation is already set
+ if (permission == 'add'
+ and rschema.cardinality(eschema, targetschemas[0], role) in '1?'
+ and eid and self.edited_entity.related(rschema.type, role)
+ and not rschema.has_perm(self.req, 'delete', fromeid=eid,
+ toeid=self.edited_entity.related(rschema.type, role)[0][0])):
+ continue
+ elif role == 'object':
+ if not ((eid is None and rschema.has_local_role(permission)) or
+ rschema.has_perm(self.req, permission, toeid=eid)):
+ continue
+ # on relation with cardinality 1 or ?, we need delete perm as well
+ # if the relation is already set
+ if (permission == 'add'
+ and rschema.cardinality(targetschemas[0], eschema, role) in '1?'
+ and eid and self.edited_entity.related(rschema.type, role)
+ and not rschema.has_perm(self.req, 'delete', toeid=eid,
+ fromeid=self.edited_entity.related(rschema.type, role)[0][0])):
+ continue
+ yield (rschema, targetschemas, role)
+
+ def srelations_by_category(self, categories=None, permission=None):
+ """filter out result of relations_by_category(categories, permission) by
+ removing final relations
- When generating a new attribute_input, the editor will look for a method
- named 'default_ATTRNAME' on the entity instance, where ATTRNAME is the
- name of the attribute being edited. You may use this feature to compute
- dynamic default values such as the 'tomorrow' date or the user's login
- being connected
- """
+ return a list of (relation's label, relation'schema, role)
+ """
+ result = []
+ for rschema, ttypes, role in self.relations_by_category(categories,
+ permission):
+ if rschema.is_final():
+ continue
+ result.append( (rschema.display_name(self.req, role), rschema, role) )
+ return sorted(result)
+
+ def relations_table(self):
+ """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
+ """
+ entity = self.edited_entity
+ pending_deletes = self.req.get_pending_deletes(entity.eid)
+ # XXX add metadata category quick fix to get Folder relations
+ for label, rschema, role in self.srelations_by_category(('generic', 'metadata'), 'add'):
+ relatedrset = entity.related(rschema, role, limit=self.limit)
+ if rschema.has_perm(self.req, 'delete'):
+ toggable_rel_link_func = toggable_relation_link
+ else:
+ toggable_rel_link_func = lambda x, y, z: u''
+ related = []
+ for row in xrange(relatedrset.rowcount):
+ nodeid = relation_id(entity.eid, rschema, role,
+ relatedrset[row][0])
+ if nodeid in pending_deletes:
+ status = u'pendingDelete'
+ label = '+'
+ else:
+ status = u''
+ label = 'x'
+ dellink = toggable_rel_link_func(entity.eid, nodeid, label)
+ eview = self.view('oneline', relatedrset, row=row)
+ related.append((nodeid, dellink, status, eview))
+ yield (rschema, role, related)
+
+ def restore_pending_inserts(self, cell=False):
+ """used to restore edition page as it was before clicking on
+ 'search for <some entity type>'
+ """
+ eid = self.edited_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) == 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
+
+ # should_* method extracted to allow overriding
+
+ def should_inline_relation_form(self, rschema, targettype, role):
+ """return true if the given relation with entity has role and a
+ targettype target should be inlined
+ """
+ return self.inlined.etype_rtag(self.edited_entity.id, role, rschema, targettype)
+
+ def should_display_inline_creation_form(self, rschema, existant, card):
+ """return true if a creation form should be inlined
+
+ by default true if there is no related entity and we need at least one
+ """
+ return not existant and card in '1+'
+
+ def should_display_add_new_relation_link(self, rschema, existant, card):
+ """return true if we should add a link to add a new creation form
+ (through ajax call)
+
+ by default true if there is no related entity or if the relation has
+ multiple cardinality
+ """
+ return not existant or card in '+*'
+
+
+class EditionFormView(EntityView):
+ """display primary entity edition form"""
id = 'edition'
- __select__ = one_line_rset() & non_final_entity()
+ # add yes() so it takes precedence over deprecated views in baseforms,
+ # though not baseforms based customized view
+ __select__ = one_line_rset() & non_final_entity() & yes()
title = _('edition')
controller = 'edit'
- skip_relations = FormMixIn.skip_relations.copy()
-
+
def cell_call(self, row, col, **kwargs):
- self.req.add_js( ('cubicweb.ajax.js',) )
- self.initialize_varmaker()
entity = self.complete_entity(row, col)
+ self.render_form(entity)
+
+ def render_form(self, entity):
+ """fetch and render the form"""
+ self.form_title(entity)
+ form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+ row=self.row, col=self.col, entity=entity,
+ domid=self.id, submitmsg=self.submited_message())
+ self.init_form(form, entity)
+ self.w(form.form_render(renderer=EntityFormRenderer(), formvid=u'edition'))
- def initialize_varmaker(self):
- varmaker = self.req.get_page_data('rql_varmaker')
- if varmaker is None:
- varmaker = self.req.varmaker
- self.req.set_page_data('rql_varmaker', varmaker)
- self.varmaker = varmaker
+ def init_form(self, form, entity):
+ """customize your form before rendering here"""
+ form.form_add_hidden(u'__maineid', entity.eid)
+
+ def form_title(self, entity):
+ """the form view title"""
+ ptitle = self.req._(self.title)
+ self.w(u'<div class="formTitle"><span>%s %s</span></div>' % (
+ entity.dc_type(), ptitle and '(%s)' % ptitle))
+
+ def submited_message(self):
+ """return the message that will be displayed on successful edition"""
+ return self.req._('entity edited')
+
- def edit_form(self, entity, kwargs):
- form = EntityFieldsForm(self.req, entity=entity)
- for rschema, target in self.editable_attributes(entity):
- field = guess_field(entity.__class__, entity.e_schema, rschema, target)
- form.fields.append(field)
- form.buttons.append(form.button_ok())
- form.buttons.append(form.button_apply())
- form.buttons.append(form.button_cancel())
- self.w(form.form_render())
+class CreationFormView(EditionFormView):
+ """display primary entity creation form"""
+ id = 'creation'
+ __select__ = specified_etype_implements('Any') & yes()
+
+ title = _('creation')
+
+ def call(self, **kwargs):
+ """creation view for an entity"""
+ etype = kwargs.pop('etype', self.req.form.get('etype'))
+ try:
+ entity = self.vreg.etype_class(etype)(self.req, None, None)
+ except:
+ self.w(self.req._('no such entity type %s') % etype)
+ else:
+ self.initialize_varmaker()
+ entity.eid = self.varmaker.next()
+ self.render_form(entity)
+
+ def form_title(self, entity):
+ """the form view title"""
+ if '__linkto' in self.req.form:
+ if isinstance(self.req.form['__linkto'], list):
+ # XXX which one should be considered (case: add a ticket to a
+ # version in jpl)
+ rtype, linkto_eid, role = self.req.form['__linkto'][0].split(':')
+ else:
+ rtype, linkto_eid, role = self.req.form['__linkto'].split(':')
+ linkto_rset = self.req.eid_rset(linkto_eid)
+ linkto_type = linkto_rset.description[0][0]
+ if role == 'subject':
+ title = self.req.__('creating %s (%s %s %s %%(linkto)s)' % (
+ entity.e_schema, entity.e_schema, rtype, linkto_type))
+ else:
+ title = self.req.__('creating %s (%s %%(linkto)s %s %s)' % (
+ entity.e_schema, linkto_type, rtype, entity.e_schema))
+ msg = title % {'linkto' : self.view('incontext', linkto_rset)}
+ self.w(u'<div class="formTitle notransform"><span>%s</span></div>' % msg)
+ else:
+ super(CreationFormView, self).form_title(entity)
+
+ def url(self):
+ """return the url associated with this view"""
+ return self.create_url(self.req.form.get('etype'))
+
+ def submited_message(self):
+ """return the message that will be displayed on successful edition"""
+ return self.req._('entity created')
+
+
+class CopyFormView(EditionFormView):
+ """display primary entity creation form initialized with values from another
+ entity
+ """
+ id = 'copy'
+ def render_form(self, entity):
+ """fetch and render the form"""
+ # make a copy of entity to avoid altering the entity in the
+ # request's cache.
+ self.newentity = copy(entity)
+ self.copying = self.newentity.eid
+ self.newentity.eid = None
+ self.w(u'<script type="text/javascript">updateMessage("%s");</script>\n'
+ % self.req._('Please note that this is only a shallow copy'))
+ super(CopyFormView, self).render_form(entity)
+ del self.newentity
+
+ def init_form(self, form, entity):
+ """customize your form before rendering here"""
+ super(CopyFormView, self).init_form(form, entity)
+ if entity.eid == self.newentity.eid:
+ form.form_add_hidden('__cloned_eid', self.copying, eidparam=True)
+
+ def submited_message(self):
+ """return the message that will be displayed on successful edition"""
+ return self.req._('entity copied')
+
+
+class TableEditForm(CompositeForm):
+ id = 'muledit'
+ onsubmit = "return validateForm('entityForm', null);"
+
+ def __init__(self, *args, **kwargs):
+ super(TableEditForm, self).__init__(*args, **kwargs)
+ for row in xrange(len(self.rset)):
+ form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
+ row=row, domid=self.id,
+ attrcategories=('primary',),
+ set_error_url=False)
+ # XXX rely on the MultipleEntityFormRenderer to put the eid input
+ form.remove_field(form.field_by_name('eid'))
+ self.form_add_subform(form)
- def editable_attributes(self, entity):
- # XXX both (add, delete)
- return [(rschema, x) for rschema, _, x in entity.relations_by_category(('primary', 'secondary'), 'add')
- if rschema != 'eid']
+ def form_buttons(self):
+ """return the form's buttons (as string)"""
+ okt = self.req._('validate modifications on selected items').capitalize()
+ resett = self.req._('revert changes').capitalize()
+ return [self.button_ok(title=okt), self.button_reset(title=resett)]
+
+
+class TableEditFormView(EntityView):
+ id = 'muledit'
+ __select__ = EntityView.__select__ & yes()
+ title = _('multiple edit')
+
+ def call(self, **kwargs):
+ """a view to edit multiple entities of the same type the first column
+ should be the eid
+ """
+ #self.form_title(entity)
+ form = self.vreg.select_object('forms', self.id, self.req, self.rset,
+ domid=self.id)
+ self.w(form.form_render(renderer=EntityCompositeFormRenderer()))
+
+
+class InlineEntityEditionFormView(EntityView):
+ id = 'inline-edition'
+ __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
+ removejs = "removeInlinedEntity('%s', '%s', '%s')"
+
+ def call(self, **kwargs):
+ """redefine default call() method to avoid automatic
+ insertions of <div class="section"> between each row of
+ the resultset
+ """
+ rset = self.rset
+ for i in xrange(len(rset)):
+ self.wview(self.id, rset, row=i, **kwargs)
+
+ def cell_call(self, row, col, peid, rtype, role='subject', **kwargs):
+ """
+ :param peid: the parent entity's eid hosting the inline form
+ :param rtype: the relation bridging `etype` and `peid`
+ :param role: the role played by the `peid` in the relation
+ """
+ entity = self.entity(row, col)
+ divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype,
+ entity.eid)
+ self.render_form(entity, peid, rtype, role, divonclick=divonclick)
+
+ def render_form(self, entity, peid, rtype, role, **kwargs):
+ """fetch and render the form"""
+ rschema = self.schema.rschema(rtype)
+ divid = '%s-%s-%s' % (peid, rtype, entity.eid)
+ title = rschema.display_name(self.req, role)
+ form = self.vreg.select_object('forms', 'edition', self.req,
+ entity=entity)
+ removejs = self.removejs % (peid, rtype,entity.eid)
+ if self.keep_entity(entity, peid, rtype):
+ if entity.has_eid():
+ rval = entity.eid
+ else:
+ rval = INTERNAL_FIELD_VALUE
+ form.form_add_hidden('edit%s-%s:%s' % (role[0], rtype, peid), rval)
+ form.form_add_hidden(name='%s:%s' % (rtype, peid), value=entity.eid,
+ id='rel-%s-%s-%s' % (peid, rtype, entity.eid))
+ self.w(form.form_render(renderer=EntityInlinedFormRenderer(), divid=divid,
+ title=title, removejs=removejs,**kwargs))
+
+ def keep_entity(self, entity, peid, rtype):
+ if not entity.has_eid():
+ return True
+ # are we regenerating form because of a validation error ?
+ erroneous_post = self.req.data.get('formvalues')
+ if erroneous_post:
+ cdvalues = self.req.list_form_param('%s:%s' % (rtype, peid),
+ erroneous_post)
+ if unicode(entity.eid) not in cdvalues:
+ return False
+ return True
+
+
+class InlineEntityCreationFormView(InlineEntityEditionFormView):
+ id = 'inline-creation'
+ __select__ = (match_kwargs('peid', 'rtype')
+ & specified_etype_implements('Any'))
+
+ def call(self, etype, peid, rtype, role='subject', **kwargs):
+ """
+ :param etype: the entity type being created in the inline form
+ :param peid: the parent entity's eid hosting the inline form
+ :param rtype: the relation bridging `etype` and `peid`
+ :param role: the role played by the `peid` in the relation
+ """
+ try:
+ entity = self.vreg.etype_class(etype)(self.req, None, None)
+ except:
+ self.w(self.req._('no such entity type %s') % etype)
+ return
+ self.initialize_varmaker()
+ entity.eid = self.varmaker.next()
+ self.render_form(entity, peid, rtype, role)