|
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) |