web/views/editforms.py
changeset 1808 aa09e20dd8c0
parent 1798 cc86fe8efaaa
child 1847 c5714f07f869
equal deleted inserted replaced
1693:49075f57cf2c 1808:aa09e20dd8c0
       
     1 """Set of HTML automatic forms to create, delete, copy or edit a single entity
       
     2 or a list of entities of the same type
       
     3 
       
     4 :organization: Logilab
       
     5 :copyright: 2001-2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     6 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     7 """
       
     8 __docformat__ = "restructuredtext en"
       
     9 
       
    10 from copy import copy
       
    11 
       
    12 from simplejson import dumps
       
    13 
       
    14 from logilab.mtconverter import html_escape
       
    15 
       
    16 from cubicweb.selectors import (match_kwargs, one_line_rset, non_final_entity,
       
    17                                 specified_etype_implements, yes)
       
    18 from cubicweb.utils import make_uid
       
    19 from cubicweb.view import EntityView
       
    20 from cubicweb.common import tags
       
    21 from cubicweb.web import INTERNAL_FIELD_VALUE, stdmsgs, eid_param
       
    22 from cubicweb.web.form import CompositeForm, EntityFieldsForm, FormViewMixIn
       
    23 from cubicweb.web.formfields import RelationField
       
    24 from cubicweb.web.formwidgets import Button, SubmitButton, ResetButton, Select
       
    25 from cubicweb.web.formrenderers import (FormRenderer, EntityFormRenderer,
       
    26                                         EntityCompositeFormRenderer,
       
    27                                         EntityInlinedFormRenderer)
       
    28 
       
    29 _ = unicode
       
    30 
       
    31 def relation_id(eid, rtype, role, reid):
       
    32     """return an identifier for a relation between two entities"""
       
    33     if role == 'subject':
       
    34         return u'%s:%s:%s' % (eid, rtype, reid)
       
    35     return u'%s:%s:%s' % (reid, rtype, eid)
       
    36 
       
    37 def toggleable_relation_link(eid, nodeid, label='x'):
       
    38     """return javascript snippet to delete/undelete a relation between two
       
    39     entities
       
    40     """
       
    41     js = u"javascript: togglePendingDelete('%s', %s);" % (
       
    42         nodeid, html_escape(dumps(eid)))
       
    43     return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (
       
    44         js, nodeid, label)
       
    45 
       
    46 
       
    47 class DeleteConfForm(FormViewMixIn, EntityView):
       
    48     """form used to confirm deletion of some entities"""
       
    49     id = 'deleteconf'
       
    50     title = _('delete')
       
    51     # don't use navigation, all entities asked to be deleted should be displayed
       
    52     # else we will only delete the displayed page
       
    53     need_navigation = False
       
    54 
       
    55     def call(self):
       
    56         """ask for confirmation before real deletion"""
       
    57         req, w = self.req, self.w
       
    58         _ = req._
       
    59         w(u'<script type="text/javascript">updateMessage(\'%s\');</script>\n'
       
    60           % _('this action is not reversible!'))
       
    61         # XXX above message should have style of a warning
       
    62         w(u'<h4>%s</h4>\n' % _('Do you want to delete the following element(s) ?'))
       
    63         form = CompositeForm(req, domid='deleteconf', copy_nav_params=True,
       
    64                              action=self.build_url('edit'), onsubmit=None,
       
    65                              form_buttons=[Button(stdmsgs.YES, cwaction='delete'),
       
    66                                            Button(stdmsgs.NO, cwaction='cancel')])
       
    67         done = set()
       
    68         w(u'<ul>\n')
       
    69         for entity in self.rset.entities():
       
    70             if entity.eid in done:
       
    71                 continue
       
    72             done.add(entity.eid)
       
    73             subform = EntityFieldsForm(req, entity=entity, set_error_url=False)
       
    74             form.form_add_subform(subform)
       
    75             # don't use outofcontext view or any other that may contain inline edition form
       
    76             w(u'<li>%s</li>' % tags.a(entity.view('textoutofcontext'),
       
    77                                       href=entity.absolute_url()))
       
    78         w(u'</ul>\n')
       
    79         w(form.form_render())
       
    80 
       
    81 
       
    82 class ClickAndEditFormView(FormViewMixIn, EntityView):
       
    83     """form used to permit ajax edition of an attribute of an entity in a view
       
    84 
       
    85     (double-click on the field to see an appropriate edition widget)
       
    86     """
       
    87     id = 'reledit'
       
    88     __select__ = non_final_entity() & match_kwargs('rtype')
       
    89 
       
    90     # FIXME editableField class could be toggleable from userprefs
       
    91 
       
    92     onsubmit = ("return inlineValidateAttributeForm('%(divid)s-form', '%(rtype)s', "
       
    93                 "'%(eid)s', '%(divid)s', %(reload)s, '%(default)s');")
       
    94     ondblclick = "showInlineEditionForm(%(eid)s, '%(rtype)s', '%(divid)s')"
       
    95 
       
    96     def cell_call(self, row, col, rtype=None, role='subject', reload=False,
       
    97                   vid='textoutofcontext', default=None):
       
    98         """display field to edit entity's `rtype` relation on double-click"""
       
    99         rschema = self.schema.rschema(rtype)
       
   100         entity = self.entity(row, col)
       
   101         if not default:
       
   102             default = self.req._('not specified')
       
   103         if rschema.is_final():
       
   104             if getattr(entity, rtype) is None:
       
   105                 value = default
       
   106             else:
       
   107                 value = entity.printable_value(rtype)
       
   108         else:
       
   109             rset = entity.related(rtype, role)
       
   110             # XXX html_escape but that depends of the actual vid
       
   111             value = html_escape(self.view(vid, rset, 'null') or default)
       
   112         if not entity.has_perm('update'):
       
   113             self.w(value)
       
   114             return
       
   115         if rschema.is_final():
       
   116             form = self._build_attribute_form(entity, value, rtype, role, reload, row, col, default)
       
   117         else:
       
   118             form = self._build_relation_form(entity, value, rtype, role, row, col, vid, default)
       
   119         form.form_add_hidden(u'__maineid', entity.eid)
       
   120         renderer = FormRenderer(display_label=False, display_help=False,
       
   121                                 display_fields=[(rtype, role)],
       
   122                                 table_class='', button_bar_class='buttonbar',
       
   123                                 display_progress_div=False)
       
   124         self.w(form.form_render(renderer=renderer))
       
   125 
       
   126     def _build_relation_form(self, entity, value, rtype, role, row, col, vid, default):
       
   127         entity = self.entity(row, col)
       
   128         divid = 'd%s' % make_uid('%s-%s' % (rtype, entity.eid))
       
   129         event_data = {'divid' : divid, 'eid' : entity.eid, 'rtype' : rtype, 'vid' : vid,
       
   130                       'default' : default, 'role' : role}
       
   131         form = EntityFieldsForm(self.req, None, entity=entity, action='#',
       
   132                                 domid='%s-form' % divid,
       
   133                                 cssstyle='display: none',
       
   134                                 onsubmit=("return inlineValidateRelationForm('%(divid)s-form', '%(rtype)s', "
       
   135                                           "'%(role)s', '%(eid)s', '%(divid)s', '%(vid)s', '%(default)s');" %
       
   136                                           event_data),
       
   137                                 form_buttons=[SubmitButton(),
       
   138                                               Button(stdmsgs.BUTTON_CANCEL,
       
   139                                                      onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" %\
       
   140                                                          (entity.eid, rtype, divid))])
       
   141         form.append_field(RelationField(name=rtype, role=role, sort=True,
       
   142                                         widget=Select(),
       
   143                                         label=u' '))
       
   144         self.w(tags.div(value, klass='editableField', id=divid,
       
   145                         ondblclick=self.ondblclick % event_data))
       
   146         return form
       
   147 
       
   148     def _build_attribute_form(self, entity, value, rtype, role, reload, row, col, default):
       
   149         eid = entity.eid
       
   150         divid = 'd%s' % make_uid('%s-%s' % (rtype, eid))
       
   151         event_data = {'divid' : divid, 'eid' : eid, 'rtype' : rtype,
       
   152                       'reload' : dumps(reload), 'default' : default}
       
   153         buttons = [SubmitButton(stdmsgs.BUTTON_OK),
       
   154                    Button(stdmsgs.BUTTON_CANCEL,
       
   155                           onclick="cancelInlineEdit(%s,\'%s\',\'%s\')" % (
       
   156                               eid, rtype, divid))]
       
   157         form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
       
   158                                        row=row, col=col, form_buttons=buttons,
       
   159                                        domid='%s-form' % divid, action='#',
       
   160                                        cssstyle='display: none',
       
   161                                        onsubmit=self.onsubmit % event_data)
       
   162         self.w(tags.div(value, klass='editableField', id=divid,
       
   163                         ondblclick=self.ondblclick % event_data))
       
   164         return form
       
   165 
       
   166 
       
   167 class EditionFormView(FormViewMixIn, EntityView):
       
   168     """display primary entity edition form"""
       
   169     id = 'edition'
       
   170     # add yes() so it takes precedence over deprecated views in baseforms,
       
   171     # though not baseforms based customized view
       
   172     __select__ = one_line_rset() & non_final_entity() & yes()
       
   173 
       
   174     title = _('edition')
       
   175     renderer = EntityFormRenderer()
       
   176 
       
   177     def cell_call(self, row, col, **kwargs):
       
   178         entity = self.complete_entity(row, col)
       
   179         self.render_form(entity)
       
   180 
       
   181     def render_form(self, entity):
       
   182         """fetch and render the form"""
       
   183         self.form_title(entity)
       
   184         form = self.vreg.select_object('forms', 'edition', self.req, entity.rset,
       
   185                                        row=entity.row, col=entity.col, entity=entity,
       
   186                                        submitmsg=self.submited_message())
       
   187         self.init_form(form, entity)
       
   188         self.w(form.form_render(renderer=self.renderer, formvid=u'edition'))
       
   189 
       
   190     def init_form(self, form, entity):
       
   191         """customize your form before rendering here"""
       
   192         form.form_add_hidden(u'__maineid', entity.eid)
       
   193 
       
   194     def form_title(self, entity):
       
   195         """the form view title"""
       
   196         ptitle = self.req._(self.title)
       
   197         self.w(u'<div class="formTitle"><span>%s %s</span></div>' % (
       
   198             entity.dc_type(), ptitle and '(%s)' % ptitle))
       
   199 
       
   200     def submited_message(self):
       
   201         """return the message that will be displayed on successful edition"""
       
   202         return self.req._('entity edited')
       
   203 
       
   204 
       
   205 class CreationFormView(EditionFormView):
       
   206     """display primary entity creation form"""
       
   207     id = 'creation'
       
   208     __select__ = specified_etype_implements('Any') & yes()
       
   209 
       
   210     title = _('creation')
       
   211 
       
   212     def call(self, **kwargs):
       
   213         """creation view for an entity"""
       
   214         etype = kwargs.pop('etype', self.req.form.get('etype'))
       
   215         try:
       
   216             entity = self.vreg.etype_class(etype)(self.req)
       
   217         except:
       
   218             self.w(self.req._('no such entity type %s') % etype)
       
   219         else:
       
   220             self.initialize_varmaker()
       
   221             entity.eid = self.varmaker.next()
       
   222             self.render_form(entity)
       
   223 
       
   224     def form_title(self, entity):
       
   225         """the form view title"""
       
   226         if '__linkto' in self.req.form:
       
   227             if isinstance(self.req.form['__linkto'], list):
       
   228                 # XXX which one should be considered (case: add a ticket to a
       
   229                 # version in jpl)
       
   230                 rtype, linkto_eid, role = self.req.form['__linkto'][0].split(':')
       
   231             else:
       
   232                 rtype, linkto_eid, role = self.req.form['__linkto'].split(':')
       
   233             linkto_rset = self.req.eid_rset(linkto_eid)
       
   234             linkto_type = linkto_rset.description[0][0]
       
   235             if role == 'subject':
       
   236                 title = self.req.__('creating %s (%s %s %s %%(linkto)s)' % (
       
   237                     entity.e_schema, entity.e_schema, rtype, linkto_type))
       
   238             else:
       
   239                 title = self.req.__('creating %s (%s %%(linkto)s %s %s)' % (
       
   240                     entity.e_schema, linkto_type, rtype, entity.e_schema))
       
   241             msg = title % {'linkto' : self.view('incontext', linkto_rset)}
       
   242             self.w(u'<div class="formTitle notransform"><span>%s</span></div>' % msg)
       
   243         else:
       
   244             super(CreationFormView, self).form_title(entity)
       
   245 
       
   246     def url(self):
       
   247         """return the url associated with this view"""
       
   248         return self.create_url(self.req.form.get('etype'))
       
   249 
       
   250     def submited_message(self):
       
   251         """return the message that will be displayed on successful edition"""
       
   252         return self.req._('entity created')
       
   253 
       
   254 
       
   255 class CopyFormView(EditionFormView):
       
   256     """display primary entity creation form initialized with values from another
       
   257     entity
       
   258     """
       
   259     id = 'copy'
       
   260     def render_form(self, entity):
       
   261         """fetch and render the form"""
       
   262         # make a copy of entity to avoid altering the entity in the
       
   263         # request's cache.
       
   264         entity.complete()
       
   265         self.newentity = copy(entity)
       
   266         self.copying = entity
       
   267         self.initialize_varmaker()
       
   268         self.newentity.eid = self.varmaker.next()
       
   269         self.w(u'<script type="text/javascript">updateMessage("%s");</script>\n'
       
   270                % self.req._('Please note that this is only a shallow copy'))
       
   271         super(CopyFormView, self).render_form(self.newentity)
       
   272         del self.newentity
       
   273 
       
   274     def init_form(self, form, entity):
       
   275         """customize your form before rendering here"""
       
   276         super(CopyFormView, self).init_form(form, entity)
       
   277         if entity.eid == self.newentity.eid:
       
   278             form.form_add_hidden(eid_param('__cloned_eid', entity.eid),
       
   279                                  self.copying.eid)
       
   280         for rschema, _, role in form.relations_by_category(form.attrcategories,
       
   281                                                            'add'):
       
   282             if not rschema.is_final():
       
   283                 # ensure relation cache is filed
       
   284                 rset = self.copying.related(rschema, role)
       
   285                 self.newentity.set_related_cache(rschema, role, rset)
       
   286 
       
   287     def submited_message(self):
       
   288         """return the message that will be displayed on successful edition"""
       
   289         return self.req._('entity copied')
       
   290 
       
   291 
       
   292 class TableEditForm(CompositeForm):
       
   293     id = 'muledit'
       
   294     domid = 'entityForm'
       
   295     onsubmit = "return validateForm('%s', null);" % domid
       
   296     form_buttons = [SubmitButton(_('validate modifications on selected items')),
       
   297                     ResetButton(_('revert changes'))]
       
   298 
       
   299     def __init__(self, req, rset, **kwargs):
       
   300         kwargs.setdefault('__redirectrql', rset.printable_rql())
       
   301         super(TableEditForm, self).__init__(req, rset, **kwargs)
       
   302         for row in xrange(len(self.rset)):
       
   303             form = self.vreg.select_object('forms', 'edition', self.req, self.rset,
       
   304                                            row=row, attrcategories=('primary',),
       
   305                                            set_error_url=False)
       
   306             # XXX rely on the EntityCompositeFormRenderer to put the eid input
       
   307             form.remove_field(form.field_by_name('eid'))
       
   308             self.form_add_subform(form)
       
   309 
       
   310 
       
   311 class TableEditFormView(FormViewMixIn, EntityView):
       
   312     id = 'muledit'
       
   313     __select__ = EntityView.__select__ & yes()
       
   314     title = _('multiple edit')
       
   315 
       
   316     def call(self, **kwargs):
       
   317         """a view to edit multiple entities of the same type the first column
       
   318         should be the eid
       
   319         """
       
   320         #self.form_title(entity)
       
   321         form = self.vreg.select_object('forms', self.id, self.req, self.rset)
       
   322         self.w(form.form_render(renderer=EntityCompositeFormRenderer()))
       
   323 
       
   324 
       
   325 class InlineEntityEditionFormView(FormViewMixIn, EntityView):
       
   326     id = 'inline-edition'
       
   327     __select__ = non_final_entity() & match_kwargs('peid', 'rtype')
       
   328     removejs = "removeInlinedEntity('%s', '%s', '%s')"
       
   329 
       
   330     def call(self, **kwargs):
       
   331         """redefine default call() method to avoid automatic
       
   332         insertions of <div class="section"> between each row of
       
   333         the resultset
       
   334         """
       
   335         rset = self.rset
       
   336         for i in xrange(len(rset)):
       
   337             self.wview(self.id, rset, row=i, **kwargs)
       
   338 
       
   339     def cell_call(self, row, col, peid, rtype, role='subject', **kwargs):
       
   340         """
       
   341         :param peid: the parent entity's eid hosting the inline form
       
   342         :param rtype: the relation bridging `etype` and `peid`
       
   343         :param role: the role played by the `peid` in the relation
       
   344         """
       
   345         entity = self.entity(row, col)
       
   346         divonclick = "restoreInlinedEntity('%s', '%s', '%s')" % (peid, rtype,
       
   347                                                                  entity.eid)
       
   348         self.render_form(entity, peid, rtype, role, divonclick=divonclick)
       
   349 
       
   350     def render_form(self, entity, peid, rtype, role, **kwargs):
       
   351         """fetch and render the form"""
       
   352         form = self.vreg.select_object('forms', 'edition', self.req, None,
       
   353                                        entity=entity, set_error_url=False)
       
   354         self.add_hiddens(form, entity, peid, rtype, role)
       
   355         divid = '%s-%s-%s' % (peid, rtype, entity.eid)
       
   356         title = self.schema.rschema(rtype).display_name(self.req, role)
       
   357         removejs = self.removejs % (peid, rtype,entity.eid)
       
   358         self.w(form.form_render(renderer=EntityInlinedFormRenderer(), divid=divid,
       
   359                                 title=title, removejs=removejs,**kwargs))
       
   360 
       
   361     def add_hiddens(self, form, entity, peid, rtype, role):
       
   362         # to ease overriding (see cubes.vcsfile.views.forms for instance)
       
   363         if self.keep_entity(form, entity, peid, rtype):
       
   364             if entity.has_eid():
       
   365                 rval = entity.eid
       
   366             else:
       
   367                 rval = INTERNAL_FIELD_VALUE
       
   368             form.form_add_hidden('edit%s-%s:%s' % (role[0], rtype, peid), rval)
       
   369         form.form_add_hidden(name='%s:%s' % (rtype, peid), value=entity.eid,
       
   370                              id='rel-%s-%s-%s'  % (peid, rtype, entity.eid))
       
   371 
       
   372     def keep_entity(self, form, entity, peid, rtype):
       
   373         if not entity.has_eid():
       
   374             return True
       
   375         # are we regenerating form because of a validation error ?
       
   376         if form.form_previous_values:
       
   377             cdvalues = self.req.list_form_param(eid_param(rtype, peid),
       
   378                                                 form.form_previous_values)
       
   379             if unicode(entity.eid) not in cdvalues:
       
   380                 return False
       
   381         return True
       
   382 
       
   383 
       
   384 class InlineEntityCreationFormView(InlineEntityEditionFormView):
       
   385     id = 'inline-creation'
       
   386     __select__ = (match_kwargs('peid', 'rtype')
       
   387                   & specified_etype_implements('Any'))
       
   388     removejs = "removeInlineForm('%s', '%s', '%s')"
       
   389 
       
   390     def call(self, etype, peid, rtype, role='subject', **kwargs):
       
   391         """
       
   392         :param etype: the entity type being created in the inline form
       
   393         :param peid: the parent entity's eid hosting the inline form
       
   394         :param rtype: the relation bridging `etype` and `peid`
       
   395         :param role: the role played by the `peid` in the relation
       
   396         """
       
   397         try:
       
   398             entity = self.vreg.etype_class(etype)(self.req, None, None)
       
   399         except:
       
   400             self.w(self.req._('no such entity type %s') % etype)
       
   401             return
       
   402         self.initialize_varmaker()
       
   403         entity.eid = self.varmaker.next()
       
   404         self.render_form(entity, peid, rtype, role)