6 """ |
6 """ |
7 __docformat__ = "restructuredtext en" |
7 __docformat__ = "restructuredtext en" |
8 |
8 |
9 from simplejson import dumps |
9 from simplejson import dumps |
10 |
10 |
|
11 from logilab.common.compat import any |
11 from logilab.mtconverter import html_escape |
12 from logilab.mtconverter import html_escape |
12 |
13 |
13 from cubicweb import typed_eid |
14 from cubicweb import typed_eid |
14 from cubicweb.selectors import match_form_params |
15 from cubicweb.selectors import match_form_params |
15 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView |
16 from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView |
16 from cubicweb.common.registerers import accepts_registerer |
17 from cubicweb.common.registerers import accepts_registerer |
17 from cubicweb.web import stdmsgs |
18 from cubicweb.web import stdmsgs |
18 from cubicweb.web.httpcache import NoHTTPCacheManager |
19 from cubicweb.web.httpcache import NoHTTPCacheManager |
19 from cubicweb.web.controller import redirect_params |
20 from cubicweb.web.controller import redirect_params |
|
21 from cubicweb.web import eid_param |
20 |
22 |
21 |
23 |
22 def relation_id(eid, rtype, target, reid): |
24 def relation_id(eid, rtype, target, reid): |
23 if target == 'subject': |
25 if target == 'subject': |
24 return u'%s:%s:%s' % (eid, rtype, reid) |
26 return u'%s:%s:%s' % (eid, rtype, reid) |
25 return u'%s:%s:%s' % (reid, rtype, eid) |
27 return u'%s:%s:%s' % (reid, rtype, eid) |
|
28 |
|
29 def toggable_relation_link(eid, nodeid, label='x'): |
|
30 js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid))) |
|
31 return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label) |
26 |
32 |
27 |
33 |
28 class FormMixIn(object): |
34 class FormMixIn(object): |
29 """abstract form mix-in""" |
35 """abstract form mix-in""" |
30 category = 'form' |
36 category = 'form' |
225 |
231 |
226 def button_reset(self, label=None, tabindex=None): |
232 def button_reset(self, label=None, tabindex=None): |
227 label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() |
233 label = self.req._(label or stdmsgs.BUTTON_CANCEL).capitalize() |
228 return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % ( |
234 return u'<input class="validateButton" type="reset" value="%s" tabindex="%s"/>' % ( |
229 label, tabindex or 4) |
235 label, tabindex or 4) |
230 |
236 |
231 def toggable_relation_link(eid, nodeid, label='x'): |
237 |
232 js = u"javascript: togglePendingDelete('%s', %s);" % (nodeid, html_escape(dumps(eid))) |
238 ############################################################################### |
233 return u'[<a class="handle" href="%s" id="handle%s">%s</a>]' % (js, nodeid, label) |
239 |
234 |
240 from cubicweb.common import tags |
|
241 |
|
242 # widgets ############ |
|
243 |
|
244 class FieldWidget(object): |
|
245 def __init__(self, attrs=None): |
|
246 self.attrs = attrs or {} |
|
247 |
|
248 def render(self, form, field): |
|
249 raise NotImplementedError |
|
250 |
|
251 class Input(FieldWidget): |
|
252 type = None |
|
253 |
|
254 def render(self, form, field): |
|
255 name, value, attrs = self._render_attrs(form, field) |
|
256 if attrs is None: |
|
257 return tags.input(name=name, value=value) |
|
258 return tags.input(name=name, value=value, type=self.type, **attrs) |
|
259 |
|
260 def _render_attrs(self, form, field): |
|
261 name = form.context[field]['name'] # qualified name |
|
262 value = form.context[field]['value'] |
|
263 #fattrs = field.widget_attributes(self) |
|
264 attrs = self.attrs.copy() |
|
265 #attrs.update(fattrs) |
|
266 # XXX id |
|
267 return name, value, attrs |
|
268 |
|
269 class TextInput(Input): |
|
270 type = 'text' |
|
271 |
|
272 class PasswordInput(Input): |
|
273 type = 'password' |
|
274 |
|
275 class FileInput(Input): |
|
276 type = 'file' |
|
277 |
|
278 class HiddenInput(Input): |
|
279 type = 'hidden' |
|
280 |
|
281 class Button(Input): |
|
282 type = 'button' |
|
283 |
|
284 class TextArea(FieldWidget): |
|
285 def render(self, form, field): |
|
286 name, value, attrs = self._render_attrs(form, field) |
|
287 if attrs is None: |
|
288 return tags.textarea(value, name=name) |
|
289 return tags.textarea(value, name=name, **attrs) |
|
290 |
|
291 class Select: |
|
292 def render(self, form, field): |
|
293 name, value, attrs = self._render_attrs(form, field) |
|
294 if self.vocabulary: |
|
295 # static vocabulary defined in form definition |
|
296 vocab = self.vocabulary |
|
297 else: |
|
298 vocab = form.get_vocabulary(field) |
|
299 options = [] |
|
300 for label, value in vocab: |
|
301 options.append(tags.option(label, value=value)) |
|
302 if attrs is None: |
|
303 return tags.select(name=name, options=options) |
|
304 return tags.select(name=name, options=options, **attrs) |
|
305 |
|
306 class CheckBox: pass |
|
307 |
|
308 class Radio: pass |
|
309 |
|
310 class DateTimePicker: pass |
|
311 |
|
312 |
|
313 # fields ############ |
|
314 |
|
315 class Field(object): |
|
316 """field class is introduced to control what's displayed in edition form |
|
317 """ |
|
318 widget = TextInput |
|
319 needs_multipart = False |
|
320 creation_rank = 0 |
|
321 |
|
322 def __init__(self, name=None, id=None, label=None, |
|
323 widget=None, required=False, initial=None, help=None, |
|
324 eidparam=True): |
|
325 self.required = required |
|
326 if widget is not None: |
|
327 self.widget = widget |
|
328 if isinstance(self.widget, type): |
|
329 self.widget = self.widget() |
|
330 self.name = name |
|
331 self.label = label or name |
|
332 self.id = id or name |
|
333 self.initial = initial |
|
334 self.help = help |
|
335 self.eidparam = eidparam |
|
336 # global fields ordering in forms |
|
337 Field.creation_rank += 1 |
|
338 |
|
339 def set_name(self, name): |
|
340 self.name = name |
|
341 if not self.id: |
|
342 self.id = name |
|
343 if not self.label: |
|
344 self.label = name |
|
345 |
|
346 def format_value(self, req, value): |
|
347 return unicode(value) |
|
348 |
|
349 def render(self, form): |
|
350 return self.widget.render(form, self) |
|
351 |
|
352 |
|
353 class StringField(Field): |
|
354 def __init__(self, max_length=None, **kwargs): |
|
355 super(StringField, self).__init__(**kwargs) |
|
356 self.max_length = max_length |
|
357 |
|
358 class TextField(Field): |
|
359 widget = TextArea |
|
360 def __init__(self, row=None, col=None, **kwargs): |
|
361 super(TextField, self).__init__(**kwargs) |
|
362 self.row = row |
|
363 self.col = col |
|
364 |
|
365 class RichTextField(Field): |
|
366 pass |
|
367 |
|
368 class IntField(Field): |
|
369 def __init__(self, min=None, max=None, **kwargs): |
|
370 super(IntField, self).__init__(**kwargs) |
|
371 self.min = min |
|
372 self.max = max |
|
373 |
|
374 class FloatField(IntField): |
|
375 |
|
376 def format_value(self, req, value): |
|
377 if value is not None: |
|
378 return ustrftime(value, req.property_value('ui.float-format')) |
|
379 return u'' |
|
380 |
|
381 class DateField(IntField): |
|
382 |
|
383 def format_value(self, req, value): |
|
384 return value and ustrftime(value, req.property_value('ui.date-format')) or u'' |
|
385 |
|
386 class DateTimeField(IntField): |
|
387 |
|
388 def format_value(self, req, value): |
|
389 return value and ustrftime(value, req.property_value('ui.datetime-format')) or u'' |
|
390 |
|
391 class FileField(IntField): |
|
392 needs_multipart = True |
|
393 |
|
394 # forms ############ |
|
395 class metafieldsform(type): |
|
396 def __new__(mcs, name, bases, classdict): |
|
397 allfields = [] |
|
398 for base in bases: |
|
399 if hasattr(base, '_fields_'): |
|
400 allfields += base._fields_ |
|
401 clsfields = (item for item in classdict.items() |
|
402 if isinstance(item[1], Field)) |
|
403 for name, field in sorted(clsfields, key=lambda x: x[1].creation_rank): |
|
404 if not field.name: |
|
405 field.set_name(name) |
|
406 allfields.append(field) |
|
407 classdict['_fields_'] = allfields |
|
408 return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict) |
|
409 |
|
410 |
|
411 class FieldsForm(object): |
|
412 __metaclass__ = metafieldsform |
|
413 |
|
414 def __init__(self, req, id=None, title=None, action='edit', |
|
415 redirect_path=None): |
|
416 self.req = req |
|
417 self.id = id or 'form' |
|
418 self.title = title |
|
419 self.action = action |
|
420 self.redirect_path = None |
|
421 self.fields = list(self.__class__._fields_) |
|
422 self.context = {} |
|
423 |
|
424 @property |
|
425 def needs_multipart(self): |
|
426 return any(field.needs_multipart for field in self.fields) |
|
427 |
|
428 def render(self, **values): |
|
429 renderer = values.pop('renderer', FormRenderer()) |
|
430 self.build_context(values) |
|
431 return renderer.render(self) |
|
432 |
|
433 def build_context(self, values): |
|
434 self.context = context = {} |
|
435 for name, field in self.fields: |
|
436 value = values.get(field.name, field.initial) |
|
437 context[field] = {'value': field.format_value(self.req, value)} |
|
438 |
|
439 def get_vocabulary(self, field): |
|
440 raise NotImplementedError |
|
441 |
|
442 |
|
443 class EntityFieldsForm(FieldsForm): |
|
444 def __init__(self, *args, **kwargs): |
|
445 kwargs.setdefault('id', 'entityForm') |
|
446 super(EntityFieldsForm, self).__init__(*args, **kwargs) |
|
447 self.fields.append(TextField(name='__type', widget=HiddenInput)) |
|
448 self.fields.append(TextField(name='eid', widget=HiddenInput)) |
|
449 |
|
450 def render(self, entity, **values): |
|
451 self.entity = entity |
|
452 return super(EntityFieldsForm, self).render(**values) |
|
453 |
|
454 def build_context(self, values): |
|
455 self.context = context = {} |
|
456 for field in self.fields: |
|
457 try: |
|
458 value = values[field.name] |
|
459 except KeyError: |
|
460 value = getattr(self.entity, field.name, field.initial) |
|
461 if field.eidparam: |
|
462 name = eid_param(field.name, self.entity.eid) |
|
463 else: |
|
464 name = field.name |
|
465 context[field] = {'value': field.format_value(self.req, value), |
|
466 'name': name} |
|
467 |
|
468 def get_vocabulary(self, field): |
|
469 choices = self.vocabfunc(entity) |
|
470 if self.sort: |
|
471 choices = sorted(choices) |
|
472 if self.rschema.rproperty(self.subjtype, self.objtype, 'internationalizable'): |
|
473 return zip((entity.req._(v) for v in choices), choices) |
|
474 return zip(choices, choices) |
|
475 |
|
476 |
|
477 # form renderers ############ |
|
478 |
|
479 class FormRenderer(object): |
|
480 def render(self, form): |
|
481 data = [] |
|
482 w = data.append |
|
483 w(u'<form action="%s" onsubmit="return freezeFormButtons(\'%s\');" method="post" id="%s">' |
|
484 % (form.req.build_url(form.action), form.id, form.id)) |
|
485 w(u'<div id="progress">%s</div>' % _('validating...')) |
|
486 w(u'<fieldset>') |
|
487 w(tags.input(type='hidden', name='__form_id', value=form.id)) |
|
488 if form.redirect_path: |
|
489 w(tags.input(type='hidden', name='__redirect_path', value=form.redirect_path)) |
|
490 for field in form.fields: |
|
491 w(field.render(form)) |
|
492 for button in form.buttons(): |
|
493 w(button.render()) |
|
494 w(u'</fieldset>') |
|
495 w(u'</form>') |
|
496 return '\n'.join(data) |