# HG changeset patch
# User sylvain.thenault@logilab.fr
# Date 1235146853 -3600
# Node ID 192800415f596127bb5075fa3c67d55f6bdfc2af
# Parent c26156f0885ecc7a762345cba7ab6f779e8bb990
FileField backport
diff -r c26156f0885e -r 192800415f59 web/form.py
--- a/web/form.py Fri Feb 20 17:20:39 2009 +0100
+++ b/web/form.py Fri Feb 20 17:20:53 2009 +0100
@@ -8,7 +8,7 @@
from warnings import warn
from simplejson import dumps
-from mx.DateTime import today
+from mx.DateTime import today, now
from logilab.common.compat import any
from logilab.mtconverter import html_escape
@@ -20,6 +20,7 @@
from cubicweb.selectors import match_form_params
from cubicweb.view import NOINDEX, NOFOLLOW, View, EntityView, AnyRsetView
from cubicweb.common.registerers import accepts_registerer
+from cubicweb.common.uilib import toggle_action
from cubicweb.web import stdmsgs
from cubicweb.web.httpcache import NoHTTPCacheManager
from cubicweb.web.controller import NAV_FORM_PARAMETERS, redirect_params
@@ -268,12 +269,12 @@
raise NotImplementedError
def _render_attrs(self, form, field):
- # name = form.form_field_name(field)
- # values = form.form_field_value(field)
name = form.context[field]['name']
values = form.context[field]['value']
if not isinstance(values, (tuple, list)):
values = (values,)
+ attrs = dict(self.attrs)
+ attrs['id'] = form.context[field]['id']
return name, values, dict(self.attrs)
class Input(FieldWidget):
@@ -291,14 +292,20 @@
class PasswordInput(Input):
type = 'password'
+ # XXX password validation
class FileInput(Input):
type = 'file'
-
+
+ def _render_attrs(self, form, field):
+ # ignore value which makes no sense here (XXX even on form validation error?)
+ name, values, attrs = super(FileInput, self)._render_attrs(form, field)
+ return name, ('',), attrs
+
class HiddenInput(Input):
type = 'hidden'
-class Button(Input):
+class ButtonInput(Input):
type = 'button'
class TextArea(FieldWidget):
@@ -313,6 +320,7 @@
raise ValueError('a textarea is not supposed to be multivalued')
return tags.textarea(value, name=name, **attrs)
+
class FCKEditor(TextArea):
def __init__(self, attrs):
super(FCKEditor, self).__init__(attrs)
@@ -347,13 +355,14 @@
options=options, **attrs)
-class CheckBox(FieldWidget):
-
+class CheckBox(Input):
+ type = 'checkbox'
+
def _render_attrs(self, form, field):
- name, value, attrs = super(CheckBox, self)._render_attrs(form, field)
- if value:
+ name, values, attrs = super(CheckBox, self)._render_attrs(form, field)
+ if values and values[0]:
attrs['checked'] = u'checked'
- return name, None, attrs
+ return name, values, attrs
class Radio(FieldWidget):
@@ -361,12 +370,11 @@
class DateTimePicker(TextInput):
- monthnames = ("january", "february", "march", "april",
- "may", "june", "july", "august",
- "september", "october", "november", "december")
-
- daynames = ("monday", "tuesday", "wednesday", "thursday",
- "friday", "saturday", "sunday")
+ monthnames = ('january', 'february', 'march', 'april',
+ 'may', 'june', 'july', 'august',
+ 'september', 'october', 'november', 'december')
+ daynames = ('monday', 'tuesday', 'wednesday', 'thursday',
+ 'friday', 'saturday', 'sunday')
needs_js = ('cubicweb.ajax.js', 'cubicweb.calendar.js')
needs_css = ('cubicweb.calendar_popup.css',)
@@ -463,53 +471,66 @@
return u''
return unicode(value)
- def get_widget(self, req):
+ def get_widget(self, form):
return self.widget
+
+ def example_format(self, req):
+ return u''
- def render(self, form):
- return self.get_widget(form.req).render(form, self)
+ def render(self, form, renderer):
+ return self.get_widget(form).render(form, self)
def vocabulary(self, form):
return self.choices
+
class StringField(Field):
def __init__(self, max_length=None, **kwargs):
super(StringField, self).__init__(**kwargs)
self.max_length = max_length
+
class TextField(Field):
+ widget = TextArea
def __init__(self, rows=10, cols=80, **kwargs):
- widget = TextArea(dict(rows=rows, cols=cols))
- super(TextField, self).__init__(widget=widget, **kwargs)
+ super(TextField, self).__init__(**kwargs)
self.rows = rows
self.cols = cols
+
class RichTextField(TextField):
widget = None
def __init__(self, format_field=None, **kwargs):
super(RichTextField, self).__init__(**kwargs)
self.format_field = format_field
- def get_widget(self, req):
+ def get_widget(self, form):
if self.widget is None:
- if self.use_fckeditor(req):
+ if self.use_fckeditor(form):
return FCKEditor()
return TextArea()
return self.widget
def get_format_field(self, form):
- if not self.format_field:
- # if fckeditor is used and format field isn't explicitly
- # deactivated, we want an hidden field for the format
+ if self.format_field:
+ return self.format_field
+ # we have to cache generated field since it's use as key in the
+ # context dictionnary
+ try:
+ return form.req.data[self]
+ except KeyError:
if self.use_fckeditor(form):
- widget = HiddenInput
- # else we want a format selector
+ # if fckeditor is used and format field isn't explicitly
+ # deactivated, we want an hidden field for the format
+ widget = HiddenInput()
else:
+ # else we want a format selector
+ # XXX compute vocabulary
widget = Select
- return StringField(name=self.name + '_format', widget=widget)
- else:
- return self.format_field
+ field = StringField(name=self.name + '_format', widget=widget)
+ form.req.data[self] = field
+ return field
def actual_fields(self, form):
yield self
@@ -525,14 +546,60 @@
return form.form_format_field_value(self) == 'text/html'
return False
- def render(self, form):
+ def render(self, form, renderer):
format_field = self.get_format_field(form)
if format_field:
- result = format_field.render(form)
+ result = format_field.render(form, renderer)
else:
result = u''
- return result + self.get_widget(form.req).render(form, self)
+ return result + self.get_widget(form).render(form, self)
+
+
+class FileField(StringField):
+ widget = FileInput
+ needs_multipart = True
+
+ def __init__(self, format_field=None, encoding_field=None, **kwargs):
+ super(FileField, self).__init__(**kwargs)
+ self.format_field = format_field
+ self.encoding_field = encoding_field
+
+ def actual_fields(self, form):
+ yield self
+ if self.format_field:
+ yield self.format_field
+ if self.encoding_field:
+ yield self.encoding_field
+ def render(self, form, renderer):
+ wdgs = [self.get_widget(form).render(form, self)]
+ if self.format_field or self.encoding_field:
+ divid = '%s-advanced' % form.context[self]['name']
+ wdgs.append(u'
' %
+ (html_escape(toggle_action(divid)),
+ form.req._('show advanced fields'),
+ html_escape(form.req.build_url('data/puce_down.png')),
+ form.req._('show advanced fields')))
+ wdgs.append(u'
' % divid)
+ if self.format_field:
+ wdgs.append(self.render_subfield(form, self.format_field, renderer))
+ if self.encoding_field:
+ wdgs.append(self.render_subfield(form, self.encoding_field, renderer))
+ wdgs.append(u'
')
+ if not self.required and form.context[self]['value']:
+ # trick to be able to delete an uploaded file
+ wdgs.append(u'
')
+ wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'],
+ type=u'checkbox'))
+ wdgs.append(form.req._('detach attached file'))
+ return u'\n'.join(wdgs)
+
+ def render_subfield(self, form, field, renderer):
+ return (renderer.render_label(form, field)
+ + field.render(form, renderer)
+ + renderer.render_help(form, field)
+ + u'
')
+
class IntField(Field):
def __init__(self, min=None, max=None, **kwargs):
@@ -540,9 +607,11 @@
self.min = min
self.max = max
+
class BooleanField(Field):
widget = Radio
+
class FloatField(IntField):
def format_single_value(self, req, value):
formatstr = entity.req.property_value('ui.float-format')
@@ -550,6 +619,10 @@
return u''
return formatstr % float(value)
+ def render_example(self, req):
+ return self.format_value(req, 1.234)
+
+
class DateField(StringField):
format_prop = 'ui.date-format'
widget = DateTimePicker
@@ -557,14 +630,17 @@
def format_single_value(self, req, value):
return value and ustrftime(value, req.property_value(self.format_prop)) or u''
+ def render_example(self, req):
+ return self.format_value(req, now())
+
+
class DateTimeField(DateField):
format_prop = 'ui.datetime-format'
+
class TimeField(DateField):
format_prop = 'ui.datetime-format'
-
-class FileField(StringField):
- needs_multipart = True
+
class HiddenInitialValueField(Field):
def __init__(self, visible_field, name):
@@ -774,8 +850,6 @@
value = values[fieldname]
elif fieldname in self.req.form:
value = self.req.form[fieldname]
- elif isinstance(field, FileField):
- return None # XXX manage FileField
else:
if self.entity.has_eid() and field.eidparam:
# use value found on the entity or field's initial value if it's
@@ -810,6 +884,13 @@
attr = field.name
if field.role == 'object':
attr += '_object'
+ else:
+ attrtype = self.entity.e_schema.destination(attr)
+ if attrtype == 'Password':
+ return self.entity.has_eid() and INTERNAL_FIELD_VALUE or ''
+ if attrtype == 'Bytes':
+ # XXX value should reflect if some file is already attached
+ return self.entity.has_eid()
if default_initial:
value = getattr(self.entity, attr, field.initial)
else:
@@ -903,7 +984,6 @@
res.append((entity.view('combobox'), entity.eid))
return res
-
class MultipleFieldsForm(FieldsForm):
def __init__(self, *args, **kwargs):
@@ -912,26 +992,48 @@
def form_add_subform(self, subform):
self.forms.append(subform)
-
+
+
# form renderers ############
class FormRenderer(object):
+
+ # renderer interface ######################################################
- def render(self, form, values):
+ def render(self, form, values, display_help=True):
data = []
w = data.append
- # XXX form_needs_multipart
w(self.open_form(form))
w(u'%s
' % form.req._('validating...'))
w(u'')
w(u'')
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'
' ]
+ descr = field.help
+ if descr:
+ help.append('%s' % req._(descr))
+ example = field.example_format(form.req)
+ if example:
+ help.append('(%s: %s)'
+ % (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'
@@ -949,12 +1051,12 @@
tag += ' cubicweb:target="%s"' % html_escape(form.cwtarget)
return tag + '>'
- def render_fields(self, w, form, values):
+ def render_fields(self, w, form, values, display_help=True):
form.form_build_context(values)
fields = form.fields[:]
for field in form.fields:
if not field.is_visible():
- w(field.render(form))
+ w(field.render(form, self))
fields.remove(field)
if fields:
w(u'')
@@ -962,26 +1064,19 @@
w(u'')
w('%s | ' % self.render_label(form, field))
w(u'')
- w(field.render(form))
+ w(field.render(form, self))
+ if display_help == True:
+ w(self.render_help(form, field))
w(u' |
')
w(u'
')
for childform in getattr(form, 'forms', []):
self.render_fields(w, childform, values)
-
- #def render_field(self, w, form, field):
def render_buttons(self, w, form):
w(u'')
-
- 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 stringfield_from_constraints(constraints, **kwargs):
diff -r c26156f0885e -r 192800415f59 web/test/unittest_form.py
--- a/web/test/unittest_form.py Fri Feb 20 17:20:39 2009 +0100
+++ b/web/test/unittest_form.py Fri Feb 20 17:20:53 2009 +0100
@@ -1,4 +1,5 @@
from logilab.common.testlib import unittest_main, mock_object
+from cubicweb import Binary
from cubicweb.devtools.testlib import WebTest
from cubicweb.web.form import *
from cubicweb.web.views.baseforms import ChangeStateForm
@@ -9,9 +10,16 @@
creation_date = DateTimeField(widget=DateTimePicker)
-class RTFStateForm(EntityFieldsForm):
+class RTFForm(EntityFieldsForm):
content = RichTextField()
+class FFForm(EntityFieldsForm):
+ data = FileField(format_field=StringField(name='data_format'),
+ encoding_field=StringField(name='data_encoding'))
+
+class PFForm(EntityFieldsForm):
+ upassword = StringField(widget=PasswordInput)
+
class EntityFieldsFormTC(WebTest):
@@ -40,8 +48,22 @@
def test_richtextfield(self):
card = self.add_entity('Card', title=u"tls sprint fev 2009",
content=u'new widgets system')
- form = CustomChangeStateForm(self.req, redirect_path='perdu.com',
- entity=card)
+ form = RTFForm(self.req, redirect_path='perdu.com',
+ entity=card)
+ self.assertEquals(form.form_render(),
+ '''''')
+
+ def test_filefield(self):
+ file = self.add_entity('File', name=u"pouet.txt",
+ data=Binary('new widgets system'))
+ form = FFForm(self.req, redirect_path='perdu.com',
+ entity=file)
+ self.assertEquals(form.form_render(),
+ '''''')
+
+ def test_passwordfield(self):
+ form = PFForm(self.req, redirect_path='perdu.com',
+ entity=self.entity)
self.assertEquals(form.form_render(),
'''''')
diff -r c26156f0885e -r 192800415f59 web/views/editcontroller.py
--- a/web/views/editcontroller.py Fri Feb 20 17:20:39 2009 +0100
+++ b/web/views/editcontroller.py Fri Feb 20 17:20:53 2009 +0100
@@ -196,7 +196,9 @@
value = Decimal(value)
elif attrtype == 'Bytes':
# if it is a file, transport it using a Binary (StringIO)
- if formparams.has_key('__%s_detach' % attr):
+ # XXX later __detach is for the new widget system, the former is to
+ # be removed once web/widgets.py has been dropped
+ if formparams.has_key('__%s_detach' % attr) or formparams.has_key('%s__detach' % attr):
# drop current file value
value = None
# no need to check value when nor explicit detach nor new file submitted,