# HG changeset patch # User Sylvain Thénault # Date 1271861627 -7200 # Node ID d321e4b62a108ba21b6c3acc8c98c6c131dd167d # Parent 4176a50c81c9a186af9c359570e1a25da5f794c0 [book] start documenting the HTML form system diff -r 4176a50c81c9 -r d321e4b62a10 doc/book/en/development/devweb/form.rst --- a/doc/book/en/development/devweb/form.rst Wed Apr 21 16:53:25 2010 +0200 +++ b/doc/book/en/development/devweb/form.rst Wed Apr 21 16:53:47 2010 +0200 @@ -1,77 +1,213 @@ -Form construction ------------------- - -CubicWeb provides usual form/field/widget/renderer abstraction to -provide some generic building blocks which will greatly help you in -building forms properly integrated with CubicWeb (coherent display, -error handling, etc...). +HTML form construction +---------------------- -A form basically only holds a set of fields, and has te be bound to a -renderer which is responsible to layout them. Each field is bound to a -widget that will be used to fill in value(s) for that field (at form -generation time) and 'decode' (fetch and give a proper Python type to) -values sent back by the browser. +CubicWeb provides the somewhat usual form / field / widget / renderer abstraction +to provide generic building blocks which will greatly help you in building forms +properly integrated with CubicWeb (coherent display, error handling, etc...), +while keeping things as flexible as possible. -The Field class and basic fields -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A **form** basically only holds a set of **fields**, and has te be bound to a +**renderer** which is responsible to layout them. Each field is bound to a +**widget** that will be used to fill in value(s) for that field (at form +generation time) and 'decode' (fetch and give a proper Python type to) values +sent back by the browser. -.. autoclass:: cubicweb.web.formfields.Field - -Existing field types are: +The **field** should be used according to the type of what you want to edit. +E.g. if you want to edit some date, you'll have to use the +:class:`~cubicweb.web.formfields.DateField`. Then you can choose among multiple +widgets to edit it, for instance :class:`~cubicweb.web.formwidgets.TextInput` (a +bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple +calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery +calendar). You can of course also write your own widget. -.. autoclass:: cubicweb.web.formfields.StringField -.. autoclass:: cubicweb.web.formfields.PasswordField -.. autoclass:: cubicweb.web.formfields.RichTextField -.. autoclass:: cubicweb.web.formfields.FileField -.. autoclass:: cubicweb.web.formfields.EditableFileField -.. autoclass:: cubicweb.web.formfields.IntField -.. autoclass:: cubicweb.web.formfields.BooleanField -.. autoclass:: cubicweb.web.formfields.FloatField -.. autoclass:: cubicweb.web.formfields.DateField -.. autoclass:: cubicweb.web.formfields.DateTimeField -.. autoclass:: cubicweb.web.formfields.TimeField -.. autoclass:: cubicweb.web.formfields.RelationField -.. autoclass:: cubicweb.web.formfields.CompoundField + +.. automodule:: cubicweb.web.formfields +.. automodule:: cubicweb.web.formwidgets +.. automodule:: cubicweb.web.views.forms +.. automodule:: cubicweb.web.views.autoform +.. automodule:: cubicweb.web.views.formrenderers -Widgets -~~~~~~~ -Base class for widget is :class:cubicweb.web.formwidgets.FieldWidget class. +Now what ? Example of bare fields form +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +We want to define a form doing something else than editing an entity. The idea is +to propose a form to send an email to entities in a resultset which implements +:class:`IEmailable`. Let's take a simplified version of what you'll find in +:mod:`cubicweb.web.views.massmailing`. + +Here is the source code: + +.. sourcecode:: python + + def sender_value(form): + return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email()) + + def recipient_choices(form, field): + return [(e.get_email(), e.eid) for e in form.cw_rset.entities() + if e.get_email()] + + def recipient_value(form): + return [e.eid for e in form.cw_rset.entities() if e.get_email()] + + class MassMailingForm(forms.FieldsForm): + __regid__ = 'massmailing' + + needs_js = ('cubicweb.widgets.js',) + domid = 'sendmail' + action = 'sendmail' -Existing widget types are: + sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}), + label=_('From:'), + value=sender_value) + + recipient = ff.StringField(widget=CheckBox(), + label=_('Recipients:'), + choices=recipient_choices, + value=recipients_value) + + subject = ff.StringField(label=_('Subject:'), max_length=256) + + mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField', + inputid='mailbody')) + + form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()", + _('send email'), 'SEND_EMAIL_ICON'), + ImgButton('cancelbutton', "javascript: history.back()", + stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')] + +Let's detail what's going on up there. Our form will hold four fields: + +* a sender field, which is disabled and will simply contains the user's name and + email + +* a recipients field, which will be displayed as a list of users in the context + result set with checkboxes so user can still choose who will receive his mailing + by checking or not the checkboxes. By default all of them will be checked since + field's value return a list containing same eids as those returned by the + vocabulary function. -.. autoclass:: cubicweb.web.formwidgets.HiddenInput -.. autoclass:: cubicweb.web.formwidgets.TextInput -.. autoclass:: cubicweb.web.formwidgets.PasswordInput -.. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput -.. autoclass:: cubicweb.web.formwidgets.FileInput -.. autoclass:: cubicweb.web.formwidgets.ButtonInput -.. autoclass:: cubicweb.web.formwidgets.TextArea -.. autoclass:: cubicweb.web.formwidgets.FCKEditor -.. autoclass:: cubicweb.web.formwidgets.Select -.. autoclass:: cubicweb.web.formwidgets.CheckBox -.. autoclass:: cubicweb.web.formwidgets.Radio -.. autoclass:: cubicweb.web.formwidgets.DateTimePicker -.. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker -.. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker -.. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker -.. autoclass:: cubicweb.web.formwidgets.AjaxWidget -.. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget -.. autoclass:: cubicweb.web.formwidgets.EditableURLWidget +* a subject field, limited to 256 characters (hence we know a + :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in + :class:`~cubicweb.web.formfields.StringField`) + +* a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`, + and whose definition won't be shown here. Notice though that we tell this form + need this javascript file by using `needs_js` + +Last but not least, we add two buttons control: one to post the form using +javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id +set to 'sendmail', which is our form DOM id as specified by its `domid` +attribute), another to cancel the form which will go back to the previous page +using another javascript call. Also we specify image to used as button icon a +resource identifier (see :ref:`external_resources`) given as last argument to +:class:`cubicweb.web.formwidgets.ImgButton`. + +To see this form, we still have to wrap it in a view. This is pretty simple: + +.. sourcecode:: python + + class MassMailingFormView(form.FormViewMixIn, EntityView): + __regid__ = 'massmailing' + __select__ = implements(IEmailable) & authenticated_user() + + def call(self): + form = self._cw.vreg['forms'].select('massmailing', self._cw, + rset=self.cw_rset) + self.w(form.render()) -Other classes in this module, which are not proper widget (they are not associated to -field) but are used as form controls, may also be useful: Button, SubmitButton, -ResetButton, ImgButton, +As you see, we simply define a view with proper selector so it only apply to a +result set containing :class:`IEmailable` entities, and so that only users in the +managers or users group can use it. Then in the `call()` method for this view we +simply select the above form and write what its `.render()` method returns. + +When this form is submitted, a controller with id 'sendmail' will be called (as +specified using `action`). This controller will be responsible to actually send +the mail to specified recipients. + +Here is what it looks like: + +.. sourcecode:: python + + class SendMailController(Controller): + __regid__ = 'sendmail' + __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject') + + def publish(self, rset=None): + body = self._cw.form['mailbody'] + subject = self._cw.form['subject'] + eids = self._cw.form['recipient'] + # eids may be a string if only one recipient was specified + if isinstance(eids, basestring): + rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids}) + else: + rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids))) + recipients = list(rset.entities()) + msg = format_mail({'email' : self._cw.user.get_email(), + 'name' : self._cw.user.dc_title()}, + recipients, body, subject) + if not self._cw.vreg.config.sendmails([(msg, recipients]): + msg = self._cw._('could not connect to the SMTP server') + else: + msg = self._cw._('emails successfully sent') + raise Redirect(self._cw.build_url(__message=msg)) -Of course you can not use any widget with any field... +The entry point of a controller is the publish method. In that case we simply get +back post values in request's `form` attribute, get user instances according +to eids found in the 'recipient' form value, and send email after calling +:func:`format_mail` to get a proper email message. If we can't send email or +if we successfully sent email, we redirect to the index page with proper message +to inform the user. -Renderers -~~~~~~~~~ +Also notice that our controller has a selector that deny access to it to +anonymous users (we don't want our instance to be used as a spam relay), but also +check expected parameters are specified in forms. That avoid later defensive +programming (though it's not enough to handle all possible error cases). + +To conclude our example, suppose we wish a different form layout and that existent +renderers are not satisfying (we would check that first of course :). We would then +have to define our own renderer: + +.. sourcecode:: python + + class MassMailingFormRenderer(formrenderers.FormRenderer): + __regid__ = 'massmailing' -.. autoclass:: cubicweb.web.views.formrenderers.BaseFormRenderer -.. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer -.. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer -.. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer -.. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer + def _render_fields(self, fields, w, form): + w(u'') + for field in fields: + if field.name == 'mailbody': + w(u'
') + w(u'
') + w(u'') + w(u'
') + w(u'
') + w(field.render(form, self)) + w(u'
') + else: + w(u'') + w(u'%s' % self.render_label(form, field)) + w(u'') + w(field.render(form, self)) + w(u'') + def render_buttons(self, w, form): + pass + +We simply override the `_render_fields` and `render_buttons` method of the base form renderer +to arrange fields as we desire it: here we'll have first a two columns table with label and +value of the sender, recipients and subject field (form order respected), then form controls, +then a div containing the textarea for the email's content. + +To bind this renderer to our form, we should add to our form definition above: + +.. sourcecode:: python + + form_renderer_id = 'massmailing' + + +.. Example of entity fields form diff -r 4176a50c81c9 -r d321e4b62a10 doc/book/en/development/devweb/views/autoform.rst --- a/doc/book/en/development/devweb/views/autoform.rst Wed Apr 21 16:53:25 2010 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,92 +0,0 @@ -The automatic entity form -------------------------- - -(:mod:`cubicweb.web.views.autoform`) - -Tags declaration -```````````````` - -It is possible to manage attributes/relations in the simple or multiple -editing form using proper uicfg tags. - -.. sourcecode:: python - - uicfg.autoform_section.tag_subject_of(, tag) - uicfg.autoform_section.tag_object_of(, tag) - uicfg.autoform_field.tag_attribute(, tag) - -The details of the uicfg syntax can be found in the :ref:`uicfg` -chapter. - -Possible tags are detailled below - -Automatic form configuration -```````````````````````````` - -Attributes/relations display location -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``uicfg.autoform_section`` specifies where to display a relation in -creation/edition entity form for a given form type. ``tag_attribute``, -``tag_subject_of`` and ``tag_object_of`` methods for this relation tag expect -two arguments additionally to the relation key: a ``formtype`` and a -``section``. - -``formtype`` may be one of: - -* ``main``, the main entity form (via the modify action) -* ``inlined``, the form for an entity inlined into another form -* ``muledit``, the table form to edit multiple entities - -section may be one of: - -* ``hidden``, don't display - -* ``attributes``, display in the attributes section - -* ``relations``, display in the relations section, using the generic relation - selector combobox (available in main form only, and not for attribute - relation) - -* ``inlined``, display target entity of the relation in an inlined form - (available in main form only, and not for attribute relation) - -* ``metadata``, display in a special metadata form (NOT YET IMPLEMENTED, subject - to changes) - -By default, mandatory relations are displayed in the ``attributes`` section, -others in ``relations`` section. - -Change default fields -^^^^^^^^^^^^^^^^^^^^^ - -Use ``autoform_field`` to replace the default field type of an attribute. - -.. warning:: - - ``autoform_field_kwargs`` should usually be used instead of - ``autoform_field``. Do not use both methods for the same relation! - - -Customize field options -^^^^^^^^^^^^^^^^^^^^^^^ - -In order to customize field options (see :class:`cubicweb.web.formfields.Field` -for a detailed list of options), use ``autoform_field_kwargs``. This rtag takes -a relation triplet and a dictionary as arguments. - -.. sourcecode:: python - - # Change the content of the combobox - # here ``ticket_done_in_choices`` is a function which returns a list of - # elements to populate the combobox - uicfg.autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'), {'sort': False, - 'choices': ticket_done_in_choices}) - - - -Overriding permissions -^^^^^^^^^^^^^^^^^^^^^^ - -``autoform_permissions_overrides`` provides a way to by-pass security checking -for dark-corner case where it can't be verified properly. XXX documents. diff -r 4176a50c81c9 -r d321e4b62a10 doc/book/en/development/devweb/views/index.rst --- a/doc/book/en/development/devweb/views/index.rst Wed Apr 21 16:53:25 2010 +0200 +++ b/doc/book/en/development/devweb/views/index.rst Wed Apr 21 16:53:47 2010 +0200 @@ -17,7 +17,6 @@ boxes table xmlrss - autoform .. editforms .. toctree:: diff -r 4176a50c81c9 -r d321e4b62a10 web/form.py --- a/web/form.py Wed Apr 21 16:53:25 2010 +0200 +++ b/web/form.py Wed Apr 21 16:53:47 2010 +0200 @@ -137,8 +137,9 @@ @iclassmethod def field_by_name(cls_or_self, name, role=None): - """return field with the given name and role. - Raise FieldNotFound if the field can't be found. + """Return field with the given name and role. + + Raise :exc:`FieldNotFound` if the field can't be found. """ for field in cls_or_self._fieldsattr(): if field.name == name and field.role == role: @@ -147,18 +148,18 @@ @iclassmethod def fields_by_name(cls_or_self, name, role=None): - """return a list of fields with the given name and role""" + """Return a list of fields with the given name and role.""" return [field for field in cls_or_self._fieldsattr() if field.name == name and field.role == role] @iclassmethod def remove_field(cls_or_self, field): - """remove a field from form class or instance""" + """Remove the given field.""" cls_or_self._fieldsattr().remove(field) @iclassmethod def append_field(cls_or_self, field): - """append a field to form class or instance""" + """Append the given field.""" cls_or_self._fieldsattr().append(field) @iclassmethod diff -r 4176a50c81c9 -r d321e4b62a10 web/formfields.py --- a/web/formfields.py Wed Apr 21 16:53:25 2010 +0200 +++ b/web/formfields.py Wed Apr 21 16:53:47 2010 +0200 @@ -1,11 +1,50 @@ -"""Fields are used to control what's displayed in forms. It makes the link -between something to edit and its display in the form. Actual display is handled -by a widget associated to the field. +# organization: Logilab +# copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +# contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +# license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +The Field class and basic fields +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. Note:: + Fields are used to control what's edited in forms. They makes the link between + something to edit and its display in the form. Actual display is handled by a + widget associated to the field. + +Let first see the base class for fields: + +.. autoclass:: cubicweb.web.formfields.Field + +Now, you usually don't use that class but one of the concret field classes +described below, according to what you want to edit. + +Basic fields +'''''''''''' -:organization: Logilab -:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +.. autoclass:: cubicweb.web.formfields.StringField() +.. autoclass:: cubicweb.web.formfields.PasswordField() +.. autoclass:: cubicweb.web.formfields.IntField() +.. autoclass:: cubicweb.web.formfields.FloatField() +.. autoclass:: cubicweb.web.formfields.BooleanField() +.. autoclass:: cubicweb.web.formfields.DateField() +.. autoclass:: cubicweb.web.formfields.DateTimeField() +.. autoclass:: cubicweb.web.formfields.TimeField() + +Compound fields +'''''''''''''''' + +.. autoclass:: cubicweb.web.formfields.RichTextField() +.. autoclass:: cubicweb.web.formfields.FileField() +.. autoclass:: cubicweb.web.formfields.CompoundField() + +.. autoclass cubicweb.web.formfields.EditableFileField() XXX should be a widget + +Entity specific fields and function +''''''''''''''''''''''''''''''''''' + +.. autoclass:: cubicweb.web.formfields.RelationField() +.. autofunction:: cubicweb.web.formfields.guess_field + """ __docformat__ = "restructuredtext en" @@ -53,48 +92,73 @@ of attributes which may be used for fine control of the behaviour of a concret field. + **Attributes** + All the attributes described below have sensible default value which may be - overriden by value given to field's constructor. + overriden by named arguments given to field's constructor. - :name: - name of the field (basestring), should be unique in a form. - :id: - dom identifier (default to the same value as `name`), should be unique in + :attr:`name` + base name of the field (basestring). The actual input name is returned by + the :meth:`input_name` method and may differ from that name (for instance + if `eidparam` is true). + :attr:`id` + DOM identifier (default to the same value as `name`), should be unique in a form. - :label: + :attr:`label` label of the field (default to the same value as `name`). - :help: + :attr:`help` help message about this field. - :widget: + :attr:`widget` widget associated to the field. Each field class has a default widget class which may be overriden per instance. - :required: + :attr:`value` + field value. May be an actual value or a callable which should take the + form as argument and return a value. + :attr:`choices` + static vocabulary for this field. May be a list of values, a list of + (label, value) tuples or a callable which should take the form and field + as arguments and return a list of values or a list of (label, value). + :attr:`required` bool flag telling if the field is required or not. - :value: - field value (may be an actual value, a default value or nothing) - :choices: - static vocabulary for this field. May be a list of values or a list of - (label, value) tuples if specified. - :sort: + :attr:`sort` bool flag telling if the vocabulary (either static vocabulary specified in `choices` or dynamic vocabulary fetched from the form) should be sorted on label. - :internationalizable: + :attr:`internationalizable` bool flag telling if the vocabulary labels should be translated using the current request language. - :eidparam: + :attr:`eidparam` bool flag telling if this field is linked to a specific entity - :role: + :attr:`role` when the field is linked to an entity attribute or relation, tells the role of the entity in the relation (eg 'subject' or 'object') - :fieldset: + :attr:`fieldset` optional fieldset to which this field belongs to - :order: + :attr:`order` key used by automatic forms to sort fields - :ignore_req_params: + :attr:`ignore_req_params` when true, this field won't consider value potentialy specified using request's form parameters (eg you won't be able to specify a value using for instance url like http://mywebsite.com/form?field=value) + + .. currentmodule:: cubicweb.web.formfields + + **Generic methods** + + .. automethod:: Field.input_name + .. automethod:: Field.dom_id + .. automethod:: Field.actual_fields + + **Form generation methods** + + .. automethod:: form_init + .. automethod:: typed_value + + **Post handling methods** + + .. automethod:: process_posted + .. automethod:: process_form_value + """ # default widget associated to this class of fields. May be overriden per # instance @@ -164,8 +228,11 @@ return not isinstance(self.widget, fw.HiddenInput) def actual_fields(self, form): - """return actual fields composing this field in case of a compound - field, usually simply return self + """Fields may be composed of other fields. For instance the + :class:`~cubicweb.web.formfields.RichTextField` is containing a format + field to define the text format. This method returns actual fields that + should be considered for display / edition. It usually simply return + self. """ yield self @@ -190,7 +257,10 @@ return self.widget def input_name(self, form, suffix=None): - """return 'qualified name' for this field""" + """Return the 'qualified name' for this field, e.g. something suitable + to use as HTML input name. You can specify a suffix that will be + included in the name when widget needs several inputs. + """ # caching is necessary else we get some pb on entity creation : # entity.eid is modified from creation mark (eg 'X') to its actual eid # (eg 123), and then `field.input_name()` won't return the right key @@ -219,7 +289,10 @@ return self.name def dom_id(self, form, suffix=None): - """return an html dom identifier for this field""" + """Return the HTML DOM identifier for this field, e.g. something + suitable to use as HTML input id. You can specify a suffix that will be + included in the name when widget needs several inputs. + """ id = self.id or self.role_name() if suffix is not None: id += suffix @@ -228,6 +301,8 @@ return id def typed_value(self, form, load_bytes=False): + """Return the correctly typed value for this field in the form context. + """ if self.eidparam and self.role is not None: entity = form.edited_entity if form._cw.vreg.schema.rschema(self.name).final: @@ -324,8 +399,8 @@ return form._cw.encoding def form_init(self, form): - """method called before by build_context to trigger potential field - initialization requiring the form instance + """Method called at form initialization to trigger potential field + initialization requiring the form instance. Do nothing by default. """ pass @@ -359,7 +434,7 @@ return True def process_form_value(self, form): - """process posted form and return correctly typed value""" + """Return the correctly typed value posted for this field.""" try: return form.formvalues[(self, form)] except KeyError: @@ -379,6 +454,9 @@ return value or None def process_posted(self, form): + """Return an iterator on (field, value) that has been posted for + field returned by :meth:`~cubicweb.web.formfields.Field.actual_fields`. + """ for field in self.actual_fields(form): if field is self: try: @@ -396,6 +474,20 @@ class StringField(Field): + """Use this field to edit unicode string (`String` yams type). This field + additionaly support a `max_length` attribute that specify a maximum size for + the string (`None` meaning no limit). + + Unless explicitly specified, the widget for this field will be: + + * :class:`~cubicweb.web.formwidgets.Select` if some vocabulary is specified + using `choices` attribute + + * :class:`~cubicweb.web.formwidgets.TextInput` if maximum size is specified + using `max_length` attribute and this length is inferior to 257. + + * :class:`~cubicweb.web.formwidgets.TextArea` in all other cases + """ widget = fw.TextArea size = 45 @@ -428,6 +520,12 @@ class PasswordField(StringField): + """Use this field to edit password (`Password` yams type, encoded python + string). + + Unless explicitly specified, the widget for this field will be + a :class:`~cubicweb.web.formwidgets.PasswordInput`. + """ widget = fw.PasswordInput def form_init(self, form): if self.eidparam and form.edited_entity.has_eid(): @@ -445,6 +543,17 @@ class RichTextField(StringField): + """This compound field allow edition of text (unicode string) in + a particular format. It has an inner field holding the text format, + that can be specified using `format_field` argument. If not specified + one will be automaticall generated. + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.FCKEditor` or a + :class:`~cubicweb.web.formwidgets.TextArea`. according to the field's + format and to user's preferences. + """ + widget = None def __init__(self, format_field=None, **kwargs): super(RichTextField, self).__init__(**kwargs) @@ -516,6 +625,17 @@ class FileField(StringField): + """This compound field allow edition of binary stream (`Bytes` yams + type). Three inner fields may be specified: + + * `format_field`, holding the file's format. + * `encoding_field`, holding the file's content encoding. + * `name_field`, holding the file's name. + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.FileInput`. Inner fields, if any, + will be added to a drop down menu at the right of the file input. + """ widget = fw.FileInput needs_multipart = True @@ -604,6 +724,15 @@ # XXX turn into a widget class EditableFileField(FileField): + """This compound field allow edition of binary stream as + :class:`~cubicweb.web.formfields.FileField` but expect that stream to + actually contains some text. + + If the stream format is one of text/plain, text/html, text/rest, + then a :class:`~cubicweb.web.formwidgets.TextArea` will be additionaly + displayed, allowing to directly the file's content when desired, instead + of choosing a file from user's file system. + """ editable_formats = ('text/plain', 'text/html', 'text/rest') def render(self, form, renderer): @@ -641,6 +770,13 @@ class IntField(Field): + """Use this field to edit integers (`Int` yams type). This field additionaly + support `min` and `max` attributes that specify a minimum and/or maximum + value for the integer (`None` meaning no boundary). + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.TextInput`. + """ def __init__(self, min=None, max=None, **kwargs): super(IntField, self).__init__(**kwargs) self.min = min @@ -662,6 +798,12 @@ class BooleanField(Field): + """Use this field to edit booleans (`Boolean` yams type). + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.Radio` with yes/no values. You + can change that values by specifing `choices`. + """ widget = fw.Radio def vocabulary(self, form): @@ -674,6 +816,13 @@ class FloatField(IntField): + """Use this field to edit floats (`Float` yams type). This field additionaly + support `min` and `max` attributes as the + :class:`~cubicweb.web.formfields.IntField`. + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.TextInput`. + """ def format_single_value(self, req, value): formatstr = req.property_value('ui.float-format') if value is None: @@ -696,6 +845,11 @@ class DateField(StringField): + """Use this field to edit date (`Date` yams type). + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.JQueryDatePicker`. + """ widget = fw.JQueryDatePicker format_prop = 'ui.date-format' etype = 'Date' @@ -721,17 +875,47 @@ class DateTimeField(DateField): + """Use this field to edit datetime (`Datetime` yams type). + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.JQueryDateTimePicker`. + """ widget = fw.JQueryDateTimePicker format_prop = 'ui.datetime-format' etype = 'Datetime' class TimeField(DateField): + """Use this field to edit time (`Time` yams type). + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.JQueryTimePicker`. + """ widget = fw.JQueryTimePicker format_prop = 'ui.time-format' etype = 'Time' +# XXX use cases where we don't actually want a better widget? +class CompoundField(Field): + """This field shouldn't be used directly, it's designed to hold inner + fields that should be conceptually groupped together. + """ + def __init__(self, fields, *args, **kwargs): + super(CompoundField, self).__init__(*args, **kwargs) + self.fields = fields + + def subfields(self, form): + return self.fields + + def actual_fields(self, form): + # don't add [self] to actual fields, compound field is usually kinda + # virtual, all interesting values are in subfield. Skipping it may avoid + # error when processed by the editcontroller : it may be marked as required + # while it has no value, hence generating a false error. + return list(self.fields) + + # relation vocabulary helper functions ######################################### def relvoc_linkedto(entity, rtype, role): @@ -786,7 +970,11 @@ class RelationField(Field): - """the relation field to edit non final relations of an entity""" + """Use this field to edit a relation of an entity. + + Unless explicitly specified, the widget for this field will be a + :class:`~cubicweb.web.formwidgets.Select`. + """ @staticmethod def fromcardinality(card, **kwargs): @@ -869,28 +1057,21 @@ eids.add(typed_eid) return eids -# XXX use cases where we don't actually want a better widget? -class CompoundField(Field): - def __init__(self, fields, *args, **kwargs): - super(CompoundField, self).__init__(*args, **kwargs) - self.fields = fields - - def subfields(self, form): - return self.fields - - def actual_fields(self, form): - # don't add [self] to actual fields, compound field is usually kinda - # virtual, all interesting values are in subfield. Skipping it may avoid - # error when processed by the editcontroller : it may be marked as required - # while it has no value, hence generating a false error. - return list(self.fields) - _AFF_KWARGS = uicfg.autoform_field_kwargs def guess_field(eschema, rschema, role='subject', skip_meta_attr=True, **kwargs): - """return the most adapated widget to edit the relation - 'subjschema rschema objschema' according to information found in the schema + """This function return the most adapted field to edit the given relation + (`rschema`) where the given entity type (`eschema`) is the subject or object + (`role`). + + The field is initialized according to information found in the schema, + though any value can be explicitly specified using `kwargs`. + + The `skip_meta_attr` flag is used to specify wether this function should + return a field for attributes considered as a meta-attributes + (e.g. describing an other attribute, such as the format or file name of a + file (`Bytes`) attribute). """ fieldclass = None rdef = eschema.rdef(rschema, role) diff -r 4176a50c81c9 -r d321e4b62a10 web/formwidgets.py --- a/web/formwidgets.py Wed Apr 21 16:53:25 2010 +0200 +++ b/web/formwidgets.py Wed Apr 21 16:53:47 2010 +0200 @@ -1,9 +1,75 @@ -"""widget classes for form construction +# organization: Logilab +# copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. +# contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +# license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +""" +Widgets +~~~~~~~ + +.. Note:: + A widget is responsible for the display of a field. It may use more than one + HTML input tags. When the form is posted, a widget is also reponsible to give + back to the field something it can understand. + + Of course you can not use any widget with any field... + +.. autoclass:: cubicweb.web.formwidgets.FieldWidget + +HTML based widgets +'''''''''''''''''''''''''' + +.. autoclass:: cubicweb.web.formwidgets.HiddenInput +.. autoclass:: cubicweb.web.formwidgets.TextInput +.. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput +.. autoclass:: cubicweb.web.formwidgets.FileInput +.. autoclass:: cubicweb.web.formwidgets.ButtonInput + +Other standard HTML widgets +''''''''''''''''''''''''''' + +.. autoclass:: cubicweb.web.formwidgets.TextArea +.. autoclass:: cubicweb.web.formwidgets.Select +.. autoclass:: cubicweb.web.formwidgets.CheckBox +.. autoclass:: cubicweb.web.formwidgets.Radio -:organization: Logilab -:copyright: 2009-2010 LOGILAB S.A. (Paris, FRANCE), license is LGPL v2. -:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr -:license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses +Date and time widgets +''''''''''''''''''''' + +.. autoclass:: cubicweb.web.formwidgets.DateTimePicker +.. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker +.. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker +.. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker + +Ajax / javascript widgets +''''''''''''''''''''''''' + +.. autoclass:: cubicweb.web.formwidgets.FCKEditor +.. autoclass:: cubicweb.web.formwidgets.AjaxWidget +.. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget + +.. kill or document AddComboBoxWidget +.. kill or document StaticFileAutoCompletionWidget +.. kill or document LazyRestrictedAutoCompletionWidget +.. kill or document RestrictedAutoCompletionWidget + +Other widgets +''''''''''''' +.. autoclass:: cubicweb.web.formwidgets.PasswordInput +.. autoclass:: cubicweb.web.formwidgets.IntervalWidget +.. autoclass:: cubicweb.web.formwidgets.HorizontalLayoutWidget +.. autoclass:: cubicweb.web.formwidgets.EditableURLWidget + +Form controls +''''''''''''' +Those classes are not proper widget (they are not associated to +field) but are used as form controls. Their API is similar +to widgets except that `field` argument given to :meth:`render` +will be `None`. + +.. autoclass:: cubicweb.web.formwidgets.Button +.. autoclass:: cubicweb.web.formwidgets.SubmitButton +.. autoclass:: cubicweb.web.formwidgets.ResetButton +.. autoclass:: cubicweb.web.formwidgets.ImgButton """ __docformat__ = "restructuredtext en" @@ -19,14 +85,50 @@ class FieldWidget(object): - """abstract widget class""" - # javascript / css files required by the widget + """The abstract base class for widgets. + + **Attributes** + + Here are standard attributes of a widget, that may be set on concret + class to override default behaviours: + + :attr:`needs_js` + list of javascript files needed by the widget. + :attr:`needs_css` + list of css files needed by the widget. + :attr:`setdomid` + flag telling if HTML DOM identifier should be set on input. + :attr:`settabindex` + flag telling if HTML tabindex attribute of inputs should be set. + :attr:`suffix` + string to use a suffix when generating input, to ease usage as a + sub-widgets (eg widget used by another widget) + :attr:`vocabulary_widget` + flag telling if this widget expect a vocabulary + + Also, widget instances takes as first argument a `attrs` dictionary which + will be stored in the attribute of the same name. It contains HTML + attributes that should be set in the widget's input tag (though concret + classes may ignore it). + + .. currentmodule:: cubicweb.web.formwidgets + + **Form generation methods** + + .. automethod:: render + .. automethod:: _render + .. automethod:: values + .. automethod:: attributes + + **Post handling methods** + + .. automethod:: process_field_data + + """ needs_js = () needs_css = () - # automatically set id and tabindex attributes ? setdomid = True settabindex = True - # to ease usage as a sub-widgets (eg widget used by another widget) suffix = None # does this widget expect a vocabulary vocabulary_widget = False @@ -51,12 +153,19 @@ if self.needs_css: form._cw.add_css(self.needs_css) + def render(self, form, field, renderer=None): + """Called to render the widget for the given `field` in the given + `form`. Return a unicode string containing the HTML snippet. - def render(self, form, field, renderer=None): + You will usually prefer to override the :meth:`_render` method so you + don't have to handle addition of needed javascript / css files. + """ self.add_media(form) return self._render(form, field, renderer) def _render(self, form, field, renderer): + """This is the method you have to implement in concret widget classes. + """ raise NotImplementedError() def format_value(self, form, field, value): @@ -75,6 +184,30 @@ return attrs def values(self, form, field): + """Return the current *string* values (i.e. for display in an HTML + string) for the given field. This method returns a list of values since + it's suitable for all kind of widgets, some of them taking multiple + values, but you'll get a single value in the list in most cases. + + Those values are searched in: + + 1. previously submitted form values if any (on validation error) + + 2. req.form (specified using request parameters) + + 3. extra form values given to form.render call (specified the code + generating the form) + + 4. field's typed value (returned by its + :meth:`~cubicweb.web.formfields.Field.typed_value` method) + + Values found in 1. and 2. are expected te be already some 'display + value' (eg a string) while those found in 3. and 4. are expected to be + correctly typed value. + + 3 and 4 are handle by the :meth:`typed_value` method to ease reuse in + concret classes. + """ values = None if not field.ignore_req_params: qname = field.input_name(form, self.suffix) @@ -112,6 +245,10 @@ return field.typed_value(form) def process_field_data(self, form, field): + """Return process posted value(s) for widget and return something + understandable by the associated `field`. That value may be correctly + typed or a string that the field may parse. + """ posted = form._cw.form val = posted.get(field.input_name(form, self.suffix)) if isinstance(val, basestring): @@ -152,13 +289,29 @@ # basic html widgets ########################################################### class TextInput(Input): - """""" + """Simple , will return an unicode string.""" type = 'text' +class PasswordSingleInput(Input): + """Simple , will return an utf-8 encoded string. + + You may prefer using the :class:`~cubicweb.web.formwidgets.PasswordInput` + widget which handles password confirmation. + """ + type = 'password' + + def process_field_data(self, form, field): + value = super(PasswordSingleInput, self).process_field_data(form, field) + if value is not None: + return value.encode('utf-8') + return value + + class PasswordInput(Input): - """ and its confirmation field (using - -confirm as name) + """ and a confirmation input. Form processing will + fail if password and confirmation differs, else it will return the password + as an utf-8 encoded string. """ type = 'password' @@ -186,19 +339,11 @@ raise ProcessFormError(form._cw._("password and confirmation don't match")) -class PasswordSingleInput(Input): - """ without a confirmation field""" - type = 'password' - - def process_field_data(self, form, field): - value = super(PasswordSingleInput, self).process_field_data(form, field) - if value is not None: - return value.encode('utf-8') - return value - - class FileInput(Input): - """""" + """Simple , will return a tuple (name, stream) where + name is the posted file name and stream a file like object containing the + posted file data. + """ type = 'file' def values(self, form, field): @@ -207,23 +352,25 @@ class HiddenInput(Input): - """""" + """Simple for hidden value, will return an unicode + string. + """ type = 'hidden' setdomid = False # by default, don't set id attribute on hidden input settabindex = False class ButtonInput(Input): - """ + """Simple , will return an unicode string. - if you want a global form button, look at the Button, SubmitButton, - ResetButton and ImgButton classes below. + If you want a global form button, look at the :class:`Button`, + :class:`SubmitButton`, :class:`ResetButton` and :class:`ImgButton` below. """ type = 'button' class TextArea(FieldWidget): - """