backport stable
authorSylvain Thénault <sylvain.thenault@logilab.fr>
Fri, 23 Apr 2010 12:42:53 +0200
changeset 5385 b6e250dd7a7d
parent 5382 cb5dfea92285 (current diff)
parent 5384 b619531ddbd2 (diff)
child 5386 4c92202ab130
backport stable
devtools/devctl.py
devtools/testlib.py
doc/book/en/development/devweb/views/autoform.rst
doc/book/en/development/devweb/views/index.rst
server/repository.py
utils.py
web/_exceptions.py
web/application.py
web/component.py
web/form.py
web/formfields.py
web/request.py
web/test/unittest_views_basecontrollers.py
web/test/unittest_views_baseviews.py
web/views/autoform.py
web/views/basecontrollers.py
web/views/tableview.py
--- a/devtools/devctl.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/devtools/devctl.py	Fri Apr 23 12:42:53 2010 +0200
@@ -590,47 +590,48 @@
     chances are the lines at the top are the ones that will bring the higher
     benefit after optimisation. Start there.
     """
-    arguments = '< rql.log'
+    arguments = 'rql.log'
     name = 'exlog'
     options = (
         )
 
     def run(self, args):
-        if args:
-            raise BadCommandUsage("no argument expected")
         import re
         requests = {}
-        for lineno, line in enumerate(sys.stdin):
-            if not ' WHERE ' in line:
-                continue
-            #sys.stderr.write( line )
+        for filepath in args:
             try:
-                rql, time = line.split('--')
-                rql = re.sub("(\'\w+': \d*)", '', rql)
-                if '{' in rql:
-                    rql = rql[:rql.index('{')]
-                req = requests.setdefault(rql, [])
-                time.strip()
-                chunks = time.split()
-                clocktime = float(chunks[0][1:])
-                cputime = float(chunks[-3])
-                req.append( (clocktime, cputime) )
-            except Exception, exc:
-                sys.stderr.write('Line %s: %s (%s)\n' % (lineno, exc, line))
-
+                stream = file(filepath)
+            except OSError, ex:
+                raise BadCommandUsage("can't open rql log file %s: %s"
+                                      % (filepath, ex))
+            for lineno, line in enumerate(file):
+                if not ' WHERE ' in line:
+                    continue
+                try:
+                    rql, time = line.split('--')
+                    rql = re.sub("(\'\w+': \d*)", '', rql)
+                    if '{' in rql:
+                        rql = rql[:rql.index('{')]
+                    req = requests.setdefault(rql, [])
+                    time.strip()
+                    chunks = time.split()
+                    clocktime = float(chunks[0][1:])
+                    cputime = float(chunks[-3])
+                    req.append( (clocktime, cputime) )
+                except Exception, exc:
+                    sys.stderr.write('Line %s: %s (%s)\n' % (lineno, exc, line))
         stat = []
-        for rql, times in requests.items():
+        for rql, times in requests.iteritems():
             stat.append( (sum(time[0] for time in times),
                           sum(time[1] for time in times),
                           len(times), rql) )
-
         stat.sort()
         stat.reverse()
-
-        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat)*0.01
+        total_time = sum(clocktime for clocktime, cputime, occ, rql in stat) * 0.01
         print 'Percentage;Cumulative Time (clock);Cumulative Time (CPU);Occurences;Query'
         for clocktime, cputime, occ, rql in stat:
-            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime, cputime, occ, rql)
+            print '%.2f;%.2f;%.2f;%s;%s' % (clocktime/total_time, clocktime,
+                                            cputime, occ, rql)
 
 
 class GenerateSchema(Command):
--- a/devtools/testlib.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/devtools/testlib.py	Fri Apr 23 12:42:53 2010 +0200
@@ -17,7 +17,10 @@
 from contextlib import contextmanager
 from warnings import warn
 
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 import yams.schema
 
@@ -488,7 +491,7 @@
 
     def remote_call(self, fname, *args):
         """remote json call simulation"""
-        dump = simplejson.dumps
+        dump = json.dumps
         args = [dump(arg) for arg in args]
         req = self.request(fname=fname, pageid='123', arg=args)
         ctrl = self.vreg['controllers'].select('json', req)
--- a/doc/book/en/admin/index.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/admin/index.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -21,6 +21,7 @@
    ldap
    pyro
    gae
+   migration
    additional-tips
 
 RQL logs
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/doc/book/en/admin/migration.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -0,0 +1,46 @@
+.. -*- coding: utf-8 -*-
+
+Migrating cubicweb instances - benefits from a distributed architecture
+=======================================================================
+
+Migrate apache & cubicweb
+-------------------------
+
+**Aim** : do the migration for N cubicweb instances hosted on a server to another with no downtime.
+
+**Prerequisites** : have an explicit definition of the database host (not default or localhost). In our case, the database is hosted on another host. You are not migrating your pyro server. You are not using multisource (more documentation on that soon).
+
+**Steps** :
+
+1. *on new machine* : install your environment (*pseudocode*) ::
+
+     apt-get install cubicweb cubicweb-applications apache2
+
+2. *on old machine* : copy your cubicweb and apache configuration to the new machine ::
+
+    scp /etc/cubicweb.d/ newmachine:/etc/cubicweb.d/
+    scp /etc/apache2/sites-available/ newmachine:/etc/apache2/sites-available/
+
+3. *on new machine* : give new ids to pyro registration so the new instances can register ::
+
+     cd /etc/cubicweb.d/ ; sed -i.bck 's/^pyro-instance-id=.*$/\02/' */all-in-one.conf
+
+4. *on new machine* : start your instances ::
+
+     cubicweb start
+
+5. *on new machine* : enable sites and modules for apache and start it, test it using by modifying your /etc/host file.
+
+6. change dns entry from your oldmachine to newmachine
+
+7. shutdown your *old machine* (if it doesn't host other services or your database)
+
+8. That's it.
+
+**Possible enhancements** : use right from the start a pound server behind your apache, that way you can add backends and smoothily migrate by shuting down backends that pound will take into account.
+
+Migrate apache & cubicweb with pyro
+-----------------------------------
+
+FIXME TODO
+
--- a/doc/book/en/development/datamodel/definition.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/development/datamodel/definition.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -42,8 +42,8 @@
 * all relationship are binary which means that to represent a
   non-binary relationship, one has to use an entity,
 * relationships do not support attributes (yet, see:
-  https://www.logilab.net/cwo/ticket/341318), hence the need to reify
-  it as an entity if need arises,
+  http://www.cubicweb.org/ticket/341318), hence the need to reify it
+  as an entity if need arises,
 * all entities have an `eid` attribute (an integer) that is its
   primary key (but it is possible to declare uniqueness on other
   attributes)
@@ -381,20 +381,20 @@
   - the permissions `add` and `delete` are equivalent. Only `add`/`read`
     are actually taken in consideration.
 
-:Note on the use of RQL expression for `add` permission:
+.. note::
 
-  Potentially, the use of an RQL expression to add an entity or a
-  relation can cause problems for the user interface, because if the
+  Potentially, the `use of an RQL expression to add an entity or a
+  relation` can cause problems for the user interface, because if the
   expression uses the entity or the relation to create, then we are
   not able to verify the permissions before we actually add the entity
   (please note that this is not a problem for the RQL server at all,
   because the permissions checks are done after the creation). In such
   case, the permission check methods (CubicWebEntitySchema.check_perm
   and has_perm) can indicate that the user is not allowed to create
-  this entity but can obtain the permission.
-  To compensate this problem, it is usually necessary, for such case,
-  to use an action that reflects the schema permissions but which enables
-  to check properly the permissions so that it would show up if necessary.
+  this entity but can obtain the permission.  To compensate this
+  problem, it is usually necessary, for such case, to use an action
+  that reflects the schema permissions but which enables to check
+  properly the permissions so that it would show up if necessary.
 
 
 Use of RQL expression for reading rights
--- a/doc/book/en/development/devweb/form.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/development/devweb/form.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -1,77 +1,213 @@
-Form construction
-------------------
-
-CubicWeb provides usual form/field/widget/renderer abstraction to
-provde 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'<table class="headersform">')
+            for field in fields:
+                if field.name == 'mailbody':
+                    w(u'</table>')
+                    w(u'<div id="toolbar">')
+                    w(u'<ul>')
+                    for button in form.form_buttons:
+                        w(u'<li>%s</li>' % button.render(form))
+                    w(u'</ul>')
+                    w(u'</div>')
+                    w(u'<div>')
+                    w(field.render(form, self))
+                    w(u'</div>')
+                else:
+                    w(u'<tr>')
+                    w(u'<td class="hlabel">%s</td>' % self.render_label(form, field))
+                    w(u'<td class="hvalue">')
+                    w(field.render(form, self))
+                    w(u'</td></tr>')
 
+        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
--- a/doc/book/en/development/devweb/publisher.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/development/devweb/publisher.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -17,7 +17,7 @@
   delegated to the `urlpublisher` component)
 
 * the controller is then selected (if not, this is considered an
-  authorization failure and signaled as sich) and called
+  authorization failure and signaled as such) and called
 
 * then either a proper result is returned, in which case the
   request/connection object issues a ``commit`` and returns the result
@@ -42,7 +42,7 @@
   can be forced from the url GET parameters), that is:
 
     * compute the `vid` using the result set and the schema (see
-      `cubicweb.web.views.vid_from_rst`)
+      `cubicweb.web.views.vid_from_rset`)
     * handle all error cases that could happen in this phase
 
 * do some cache management chores
--- a/doc/book/en/development/devweb/views/autoform.rst	Fri Apr 23 11:10:30 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(<relation>, tag)
-  uicfg.autoform_section.tag_object_of(<relation>, tag)
-  uicfg.autoform_field.tag_attribute(<attribut_def>, 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.
--- a/doc/book/en/development/devweb/views/primary.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/development/devweb/views/primary.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -210,12 +210,12 @@
 We continue along the basic tutorial :ref:`tuto_blog`.
 
 If you want to change the way a ``BlogEntry`` is displayed, just override
-the method ``cell_call()`` of the view ``primary`` in ``BlogDemo/views.py``:
+the method ``cell_call()`` of the view ``primary`` in ``BlogDemo/views.py``.
 
 .. sourcecode:: python
 
   from cubicweb.selectors import implements
-  from cubicweb.web.views.primary improt Primaryview
+  from cubicweb.web.views.primary import Primaryview
 
   class BlogEntryPrimaryView(PrimaryView):
     __select__ = PrimaryView.__select__ & implements('BlogEntry')
--- a/doc/book/en/standard_theme/static/lglb-sphinx-doc.css	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/standard_theme/static/lglb-sphinx-doc.css	Fri Apr 23 12:42:53 2010 +0200
@@ -1,8 +1,8 @@
 /**
- * Sphinx stylesheet -- Logilab theme
+ * Sphinx stylesheet -- CubicWeb theme
  * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  *
- * Inspired from sphinxdoc original theme.
+ * Inspired from sphinxdoc original theme and logilab theme.
  */
 
 @import url("basic.css");
@@ -10,24 +10,16 @@
 /* -- page layout ----------------------------------------------------------- */
 
 body {
-    font-family: 'Bitstream Vera Sans', 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva',
-                 'Verdana', sans-serif;
+    font-family: 'Bitstream Vera Sans', 'Lucida Grande', 'Lucida Sans Unicode',
+    'Geneva', 'Verdana', sans-serif;
     font-size: 14px;
     line-height: 150%;
     text-align: center;
-    background-color: #D0D0D0;
-    color: #000000;
     padding: 0;
-    border: 1px solid #CCBCA7;
-    min-width: 640px;
-    margin: 0px 30px 0px 30px;
 }
 
 div.document {
-    background-color: #FFFFFF;
     text-align: left;
-    background-image: url(contents.png);
-    background-repeat: repeat-x;
 }
 
 div.bodywrapper {
@@ -41,19 +33,21 @@
 }
 
 div.header {
-    margin: 0;
-    padding: 5px 5px 25px 5px;
-    background-color: #FFFFFF;
     text-align: left;
-    line-height: 80%;
  }
 
 div.related {
+    background-color: #FF7700;
+    color: white;
+    font-weight: bolder;
     font-size: 1em;
 }
 
+div.related a {
+    color: white;
+}
+
 div.related ul {
-    background-image: url(navigation.png);
     height: 2em;
     border-top: 1px solid #CCBCA7;
     border-bottom: 1px solid #CCBCA7;
@@ -91,21 +85,13 @@
 }
 
 div.sphinxsidebar h3, div.sphinxsidebar h4 {
-    margin: 1em 0 0.5em 0;
-    font-size: 1em;
-    padding: 0.1em 0 0.1em 0.5em;
-    color: white;
-    border: 1px solid #CCBCA7;
-    background-color: #909090;
-}
-
-div.sphinxsidebar h3 a {
-    color: white;
+    font-size: 1.2em;
+    font-style: italic;
 }
 
 div.sphinxsidebar ul {
     padding-left: 1.5em;
-    margin-top: 7px;
+    margin-top: 15px;
     padding: 0;
     line-height: 130%;
     font-weight: bold;
@@ -127,16 +113,14 @@
 }
 
 div.footer {
-    background-color: #FFFFFF;
-    color: #707070;
+    color: orangered;
     padding: 3px 8px 3px 0;
     clear: both;
     font-size: 0.8em;
-    text-align: right;
+    text-align: center;
 }
 
 div.footer a {
-    color: #707070;
     text-decoration: underline;
 }
 
@@ -155,15 +139,16 @@
 }
 
 a {
-    color: #B45300;
+    color: orangered;
+    text-decoration: none;
+}
+
+div.sphinxsidebar a {
+    color: black;
     text-decoration: none;
 }
 
 a:hover {
-    color: #4BACFF;
-}
-
-div.body a {
     text-decoration: underline;
 }
 
@@ -171,8 +156,7 @@
     margin: 0;
     padding: 0.7em 0 0.3em 0;
     font-size: 1.5em;
-    border-bottom: 3px solid #CC8B00;
-    color: #404040;
+    border-bottom: 1px dotted;
 }
 
 h2 {
--- a/doc/book/en/tutorials/advanced/index.rst	Fri Apr 23 11:10:30 2010 +0200
+++ b/doc/book/en/tutorials/advanced/index.rst	Fri Apr 23 12:42:53 2010 +0200
@@ -35,7 +35,7 @@
 I can now create the cube which will hold custom code for this web
 site using::
 
-  c-c newcube --directory=~/src/cubes sytweb
+  cubicweb-ctl newcube --directory=~/src/cubes sytweb
 
 
 .. _adv_tuto_assemble_cubes:
@@ -141,19 +141,19 @@
 Now that I've a schema, I want to create an instance so I can start To
 create an instance using this new 'sytweb' cube, I run::
 
-  c-c create sytweb sytweb_instance
+  cubicweb-ctl create sytweb sytweb_instance
 
 hint : if you get an error while the database is initialized, you can
 avoid having to reanswer to questions by runing ::
 
-   c-c db-create sytweb_instance
+   cubicweb-ctl db-create sytweb_instance
 
 This will use your already configured instance and start directly from the create
 database step, thus skipping questions asked by the 'create' command.
 
 Once the instance and database are fully initialized, run ::
 
-  c-c start sytweb_instance
+  cubicweb-ctl start sytweb_instance
 
 to start the instance, check you can connect on it, etc...
 
--- a/etwist/service.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/etwist/service.py	Fri Apr 23 12:42:53 2010 +0200
@@ -14,8 +14,10 @@
 logger = getLogger('cubicweb.twisted')
 logger.handlers = [handlers.NTEventLogHandler('cubicweb')]
 
-os.environ['CW_INSTANCES_DIR'] = r'C:\etc\cubicweb.d'
-os.environ['USERNAME'] = 'cubicweb'
+if not os.environ.get('CW_INSTANCES_DIR'):
+    os.environ['CW_INSTANCES_DIR'] = r'C:\etc\cubicweb.d'
+if not os.environ.get('USERNAME'):
+    os.environ['USERNAME'] = 'cubicweb'
 
 class CWService(object, win32serviceutil.ServiceFramework):
     _svc_name_ = None
--- a/goa/goactl.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/goa/goactl.py	Fri Apr 23 12:42:53 2010 +0200
@@ -17,7 +17,11 @@
 
 
 def slink_directories():
-    import rql, yams, yapps, simplejson, docutils, roman
+    import rql, yams, yapps, docutils, roman
+    try:
+        import json as simplejson
+    except ImportError:
+        import simplejson
     from logilab import common as lgc
     from logilab import constraint as lgcstr
     from logilab import mtconverter as lgmtc
--- a/server/repository.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/server/repository.py	Fri Apr 23 12:42:53 2010 +0200
@@ -320,9 +320,9 @@
                 looptask.join()
                 self.info('task %s finished', looptask.name)
         for thread in self._running_threads:
-            self.info('waiting thread %s...', thread.name)
+            self.info('waiting thread %s...', thread.getName())
             thread.join()
-            self.info('thread %s finished', thread.name)
+            self.info('thread %s finished', thread.getName())
         if not (self.config.creating or self.config.repairing
                 or self.config.quick_start):
             self.hm.call_hooks('server_shutdown', repo=self)
--- a/server/utils.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/server/utils.py	Fri Apr 23 12:42:53 2010 +0200
@@ -152,6 +152,5 @@
         self.daemon = True
         Thread.start(self)
 
-    @property
-    def name(self):
+    def getName(self):
         return '%s(%s)' % (self._name, Thread.getName(self))
--- a/test/unittest_utils.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/test/unittest_utils.py	Fri Apr 23 12:42:53 2010 +0200
@@ -14,10 +14,13 @@
 from cubicweb.utils import make_uid, UStringIO, SizeConstrainedList, RepeatList
 
 try:
-    import simplejson
+    try:
+        import json 
+    except ImportError:
+        import simplejson as json
     from cubicweb.utils import CubicWebJsonEncoder
 except ImportError:
-    simplejson = None
+    json = None
 
 class MakeUidTC(TestCase):
     def test_1(self):
@@ -107,11 +110,11 @@
 
 class JSONEncoderTC(TestCase):
     def setUp(self):
-        if simplejson is None:
-            self.skip('simplejson not available')
+        if json is None:
+            self.skip('json not available')
 
     def encode(self, value):
-        return simplejson.dumps(value, cls=CubicWebJsonEncoder)
+        return json.dumps(value, cls=CubicWebJsonEncoder)
 
     def test_encoding_dates(self):
         self.assertEquals(self.encode(datetime.datetime(2009, 9, 9, 20, 30)),
--- a/utils.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/utils.py	Fri Apr 23 12:42:53 2010 +0200
@@ -310,14 +310,17 @@
 
 
 try:
-    # may not be there if cubicweb-web not installed
-    from simplejson import dumps, JSONEncoder
+    try:
+        # may not be there if cubicweb-web not installed
+        from json import dumps, JSONEncoder
+    except ImportError:
+        from simplejson import dumps, JSONEncoder
 except ImportError:
     pass
 else:
 
     class CubicWebJsonEncoder(JSONEncoder):
-        """define a simplejson encoder to be able to encode yams std types"""
+        """define a json encoder to be able to encode yams std types"""
 
         # _iterencode is the only entry point I've found to use a custom encode
         # hook early enough: .default() is called if nothing else matched before,
--- a/view.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/view.py	Fri Apr 23 12:42:53 2010 +0200
@@ -12,7 +12,10 @@
 from cStringIO import StringIO
 from warnings import warn
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.common.deprecation import deprecated
 from logilab.mtconverter import xml_escape
--- a/web/__init__.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/__init__.py	Fri Apr 23 12:42:53 2010 +0200
@@ -10,7 +10,10 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 from urllib import quote as urlquote
 
 from logilab.common.deprecation import deprecated
--- a/web/_exceptions.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/_exceptions.py	Fri Apr 23 12:42:53 2010 +0200
@@ -53,8 +53,11 @@
         self.reason = reason
 
     def dumps(self):
-        import simplejson
-        return simplejson.dumps({'reason': self.reason})
+        try:
+            from json import dumps
+        except ImportError:
+            from simplejson import dumps
+        return dumps({'reason': self.reason})
 
 class LogOut(PublishException):
     """raised to ask for deauthentication of a logged in user"""
--- a/web/application.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/application.py	Fri Apr 23 12:42:53 2010 +0200
@@ -413,6 +413,9 @@
                 self.error_handler(req, ex, tb=False)
             except Exception, ex:
                 self.error_handler(req, ex, tb=True)
+            except:
+                self.critical('Catch all triggered!!!')
+                self.exception('this is what happened')
         finally:
             if req.cnx:
                 try:
--- a/web/component.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/component.py	Fri Apr 23 12:42:53 2010 +0200
@@ -8,7 +8,10 @@
 __docformat__ = "restructuredtext en"
 _ = unicode
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.common.deprecation import class_renamed
 from logilab.mtconverter import xml_escape
--- a/web/form.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/form.py	Fri Apr 23 12:42:53 2010 +0200
@@ -14,8 +14,7 @@
 
 from cubicweb.appobject import AppObject
 from cubicweb.view import NOINDEX, NOFOLLOW
-from cubicweb.web import httpcache, formfields, controller
-
+from cubicweb.web import httpcache, formfields, controller, formwidgets as fwdgs
 
 class FormViewMixIn(object):
     """abstract form view mix-in"""
@@ -139,8 +138,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:
@@ -149,31 +149,49 @@
 
     @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
-    def insert_field_before(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
+    def insert_field_before(cls_or_self, field, name, role=None):
+        """Insert the given field before the field of given name and role."""
+        bfield = cls_or_self.field_by_name(name, role)
         fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field), new_field)
+        fields.insert(fields.index(bfield), field)
+
+    @iclassmethod
+    def insert_field_after(cls_or_self, field, name, role=None):
+        """Insert the given field after the field of given name and role."""
+        afield = cls_or_self.field_by_name(name, role)
+        fields = cls_or_self._fieldsattr()
+        fields.insert(fields.index(afield)+1, field)
 
     @iclassmethod
-    def insert_field_after(cls_or_self, new_field, name, role='subject'):
-        field = cls_or_self.field_by_name(name, role)
-        fields = cls_or_self._fieldsattr()
-        fields.insert(fields.index(field)+1, new_field)
+    def add_hidden(cls_or_self, name, value=None, **kwargs):
+        """Append an hidden field to the form. `name`, `value` and extra keyword
+        arguments will be given to the field constructor. The inserted field is
+        returned.
+        """
+        kwargs.setdefault('ignore_req_params', True)
+        kwargs.setdefault('widget', fwdgs.HiddenInput)
+        field = formfields.StringField(name=name, value=value, **kwargs)
+        if 'id' in kwargs:
+            # by default, hidden input don't set id attribute. If one is
+            # explicitly specified, ensure it will be set
+            field.widget.setdomid = True
+        cls_or_self.append_field(field)
+        return field
 
     def session_key(self):
         """return the key that may be used to store / retreive data about a
--- a/web/formfields.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/formfields.py	Fri Apr 23 12:42:53 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"
 
@@ -13,6 +52,7 @@
 from datetime import datetime
 
 from logilab.mtconverter import xml_escape
+from logilab.common import nullobject
 from logilab.common.date import ustrftime
 
 from yams.schema import KNOWN_METAATTRIBUTES, role_name
@@ -45,55 +85,80 @@
     result += sorted(partresult)
     return result
 
-_MARKER = object()
+_MARKER = nullobject()
 
 class Field(object):
     """This class is the abstract base class for all fields. It hold a bunch
     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
@@ -163,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
 
@@ -189,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
@@ -218,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
@@ -227,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:
@@ -323,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
 
@@ -358,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:
@@ -378,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:
@@ -395,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
 
@@ -427,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():
@@ -444,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)
@@ -515,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
 
@@ -601,7 +722,17 @@
         return value
 
 
+# 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):
@@ -639,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
@@ -660,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):
@@ -672,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:
@@ -694,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'
@@ -719,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):
@@ -784,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):
@@ -867,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)
--- a/web/formwidgets.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/formwidgets.py	Fri Apr 23 12:42:53 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 <input> 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,37 +153,61 @@
         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):
         return field.format_value(form._cw, value)
 
-    def values_and_attributes(self, form, field):
-        """found field's *string* value in:
-        1. previously submitted form values if any (eg on validation error)
-        2. req.form
-        3. extra form values given to render()
-        4. field's typed value
-
-        values found in 1. and 2. are expected te be already some 'display'
-        value while those found in 3. and 4. are expected to be correctly typed.
-
-        3 and 4 are handle by the .typed_value(form, field) method
+    def attributes(self, form, field):
+        """Return HTML attributes for the widget, automatically setting DOM
+        identifier and tabindex when desired (see :attr:`setdomid` and
+        :attr:`settabindex` attributes)
         """
         attrs = dict(self.attrs)
         if self.setdomid:
             attrs['id'] = field.dom_id(form, self.suffix)
         if self.settabindex and not 'tabindex' in attrs:
             attrs['tabindex'] = form._cw.next_tabindex()
-        return self.values(form, field), attrs
+        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)
@@ -119,12 +245,20 @@
         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):
             val = val.strip()
         return val
 
+    # XXX deprecates
+    def values_and_attributes(self, form, field):
+        return self.values(form, field), self.attributes(form, field)
+
     @deprecated('[3.6] use values_and_attributes')
     def _render_attrs(self, form, field):
         """return html tag name, attributes and a list of values for the field
@@ -155,13 +289,29 @@
 # basic html widgets ###########################################################
 
 class TextInput(Input):
-    """<input type='text'>"""
+    """Simple <input type='text'>, will return an unicode string."""
     type = 'text'
 
 
+class PasswordSingleInput(Input):
+    """Simple <input type='password'>, 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):
-    """<input type='password'> and its confirmation field (using
-    <field's name>-confirm as name)
+    """<input type='password'> 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'
 
@@ -189,45 +339,38 @@
         raise ProcessFormError(form._cw._("password and confirmation don't match"))
 
 
-class PasswordSingleInput(Input):
-    """<input type='password'> 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):
-    """<input type='file'>"""
+    """Simple <input type='file'>, 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_and_attributes(self, form, field):
+    def values(self, form, field):
         # ignore value which makes no sense here (XXX even on form validation error?)
-        values, attrs = super(FileInput, self).values_and_attributes(form, field)
-        return ('',), attrs
+        return ('',)
 
 
 class HiddenInput(Input):
-    """<input type='hidden'>"""
+    """Simple <input type='hidden'> 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):
-    """<input type='button'>
+    """Simple <input type='button'>, 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):
-    """<textarea>"""
+    """Simple <textarea>, will return an unicode string."""
 
     def _render(self, form, field, renderer):
         values, attrs = self.values_and_attributes(form, field)
@@ -249,7 +392,9 @@
 
 
 class FCKEditor(TextArea):
-    """FCKEditor enabled <textarea>"""
+    """FCKEditor enabled <textarea>, will return an unicode string containing
+    HTML formated text.
+    """
     def __init__(self, *args, **kwargs):
         super(FCKEditor, self).__init__(*args, **kwargs)
         self.attrs['cubicweb:type'] = 'wysiwyg'
@@ -260,14 +405,16 @@
 
 
 class Select(FieldWidget):
-    """<select>, for field having a specific vocabulary"""
+    """Simple <select>, for field having a specific vocabulary. Will return
+    an unicode string, or a list of unicode strings.
+    """
     vocabulary_widget = True
 
     def __init__(self, attrs=None, multiple=False, **kwargs):
         super(Select, self).__init__(attrs, **kwargs)
         self._multiple = multiple
 
-    def render(self, form, field, renderer):
+    def _render(self, form, field, renderer):
         curvalues, attrs = self.values_and_attributes(form, field)
         if not 'size' in attrs:
             attrs['size'] = self._multiple and '5' or '1'
@@ -298,16 +445,29 @@
 
 
 class CheckBox(Input):
-    """<input type='checkbox'>, for field having a specific vocabulary. One
-    input will be generated for each possible value.
+    """Simple <input type='checkbox'>, for field having a specific
+    vocabulary. One input will be generated for each possible value.
+
+    You can specify separator using the `separator` constructor argument, by
+    default <br/> is used.
     """
     type = 'checkbox'
     vocabulary_widget = True
 
-    def render(self, form, field, renderer):
+    def __init__(self, attrs=None, separator=u'<br/>\n', **kwargs):
+        super(CheckBox, self).__init__(attrs, **kwargs)
+        self.separator = separator
+
+    def _render(self, form, field, renderer):
         curvalues, attrs = self.values_and_attributes(form, field)
         domid = attrs.pop('id', None)
-        sep = attrs.pop('separator', u'<br/>\n')
+        # XXX turn this as initializer argument
+        try:
+            sep = attrs.pop('separator')
+            warn('[3.8] separator should be specified using initializer argument',
+                 DeprecationWarning)
+        except KeyError:
+            sep = self.separator
         options = []
         for i, option in enumerate(field.vocabulary(form)):
             try:
@@ -328,62 +488,20 @@
 
 
 class Radio(CheckBox):
-    """<input type='radio'>, for field having a specific vocabulary. One
+    """Simle <input type='radio'>, for field having a specific vocabulary. One
     input will be generated for each possible value.
+
+    You can specify separator using the `separator` constructor argument, by
+    default <br/> is used.
     """
     type = 'radio'
 
 
-# compound widgets #############################################################
-
-class IntervalWidget(FieldWidget):
-    """custom widget to display an interval composed by 2 fields. This widget
-    is expected to be used with a CompoundField containing the two actual
-    fields.
-
-    Exemple usage::
-
-from uicfg import autoform_field, autoform_section
-autoform_field.tag_attribute(('Concert', 'minprice'),
-                              CompoundField(fields=(IntField(name='minprice'),
-                                                    IntField(name='maxprice')),
-                                            label=_('price'),
-                                            widget=IntervalWidget()
-                                            ))
-# we've to hide the other field manually for now
-autoform_section.tag_attribute(('Concert', 'maxprice'), 'generated')
-    """
-    def render(self, form, field, renderer):
-        actual_fields = field.fields
-        assert len(actual_fields) == 2
-        return u'<div>%s %s %s %s</div>' % (
-            form._cw._('from_interval_start'),
-            actual_fields[0].render(form, renderer),
-            form._cw._('to_interval_end'),
-            actual_fields[1].render(form, renderer),
-            )
-
-
-class HorizontalLayoutWidget(FieldWidget):
-    """custom widget to display a set of fields grouped together horizontally
-    in a form. See `IntervalWidget` for example usage.
-    """
-    def render(self, form, field, renderer):
-        if self.attrs.get('display_label', True):
-            subst = self.attrs.get('label_input_substitution', '%(label)s %(input)s')
-            fields = [subst % {'label': renderer.render_label(form, f),
-                              'input': f.render(form, renderer)}
-                      for f in field.subfields(form)]
-        else:
-            fields = [f.render(form, renderer) for f in field.subfields(form)]
-        return u'<div>%s</div>' % ' '.join(fields)
-
-
 # javascript widgets ###########################################################
 
 class DateTimePicker(TextInput):
-    """<input type='text' + javascript date/time picker for date or datetime
-    fields
+    """<input type='text'> + javascript date/time picker for date or datetime
+    fields. Will return the date or datetime as an unicode string.
     """
     monthnames = ('january', 'february', 'march', 'april',
                   'may', 'june', 'july', 'august',
@@ -397,15 +515,14 @@
     @classmethod
     def add_localized_infos(cls, req):
         """inserts JS variables defining localized months and days"""
-        # import here to avoid dependancy from cubicweb to simplejson
         _ = req._
         monthnames = [_(mname) for mname in cls.monthnames]
         daynames = [_(dname) for dname in cls.daynames]
         req.html_headers.define_var('MONTHNAMES', monthnames)
         req.html_headers.define_var('DAYNAMES', daynames)
 
-    def render(self, form, field, renderer):
-        txtwidget = super(DateTimePicker, self).render(form, field, renderer)
+    def _render(self, form, field, renderer):
+        txtwidget = super(DateTimePicker, self)._render(form, field, renderer)
         self.add_localized_infos(form._cw)
         cal_button = self._render_calendar_popup(form, field)
         return txtwidget + cal_button
@@ -425,7 +542,9 @@
 
 
 class JQueryDatePicker(FieldWidget):
-    """use jquery.ui.datepicker to define a date time picker"""
+    """Use jquery.ui.datepicker to define a date picker. Will return the date as
+    an unicode string.
+    """
     needs_js = ('jquery.ui.js', )
     needs_css = ('jquery.ui.css',)
 
@@ -452,7 +571,9 @@
 
 
 class JQueryTimePicker(FieldWidget):
-    """use jquery.timePicker.js to define a js time picker"""
+    """Use jquery.timePicker to define a time picker. Will return the time as an
+    unicode string.
+    """
     needs_js = ('jquery.timePicker.js',)
     needs_css = ('jquery.timepicker.css',)
 
@@ -476,6 +597,10 @@
 
 
 class JQueryDateTimePicker(FieldWidget):
+    """Compound widget using :class:`JQueryDatePicker` and
+    :class:`JQueryTimePicker` widgets to define a date and time picker. Will
+    return the date and time as python datetime instance.
+    """
     def __init__(self, initialtime=None, timesteps=15, **kwargs):
         super(JQueryDateTimePicker, self).__init__(**kwargs)
         self.initialtime = initialtime
@@ -507,8 +632,8 @@
         timepicker = JQueryTimePicker(timestr=timestr, timesteps=self.timesteps,
                                       suffix='time')
         return u'<div id="%s">%s%s</div>' % (field.dom_id(form),
-                                            datepicker.render(form, field),
-                                            timepicker.render(form, field))
+                                            datepicker.render(form, field, renderer),
+                                            timepicker.render(form, field, renderer))
 
     def process_field_data(self, form, field):
         req = form._cw
@@ -535,7 +660,9 @@
 
 
 class AjaxWidget(FieldWidget):
-    """simple <div> based ajax widget"""
+    """Simple <div> based ajax widget, requiring a `wdgtype` argument telling
+    which javascript widget should be used.
+    """
     def __init__(self, wdgtype, inputid=None, **kwargs):
         super(AjaxWidget, self).__init__(**kwargs)
         init_ajax_attributes(self.attrs, wdgtype)
@@ -548,8 +675,10 @@
 
 
 class AutoCompletionWidget(TextInput):
-    """ajax widget for StringField, proposing matching existing values as you
-    type.
+    """<input type='text'> based ajax widget, taking a `autocomplete_initfunc`
+    argument which should specify the name of a method of the json
+    controller. This method is expected to return allowed values for the input,
+    that the widget will use to propose matching values as you type.
     """
     needs_js = ('cubicweb.widgets.js', 'jquery.autocomplete.js')
     needs_css = ('jquery.autocomplete.css',)
@@ -560,21 +689,25 @@
         try:
             self.autocomplete_initfunc = kwargs.pop('autocomplete_initfunc')
         except KeyError:
-            warn('use autocomplete_initfunc argument of %s constructor '
+            warn('[3.6] use autocomplete_initfunc argument of %s constructor '
                  'instead of relying on autocomplete_initfuncs dictionary on '
                  'the entity class' % self.__class__.__name__,
                  DeprecationWarning)
             self.autocomplete_initfunc = None
         super(AutoCompletionWidget, self).__init__(*args, **kwargs)
 
-    def values_and_attributes(self, form, field):
-        values, attrs = super(AutoCompletionWidget, self).values_and_attributes(form, field)
+    def values(self, form, field):
+        values = super(AutoCompletionWidget, self).values(form, field)
+        if not values:
+            values = ('',)
+        return values
+
+    def attributes(self, form, field):
+        attrs = super(AutoCompletionWidget, self).attributes(form, field)
         init_ajax_attributes(attrs, self.wdgtype, self.loadtype)
         # XXX entity form specific
         attrs['cubicweb:dataurl'] = self._get_url(form.edited_entity, field)
-        if not values:
-            values = ('',)
-        return values, attrs
+        return attrs
 
     def _get_url(self, entity, field):
         if self.autocomplete_initfunc is None:
@@ -609,8 +742,6 @@
     wdgtype = 'LazySuggestField'
 
     def values_and_attributes(self, form, field):
-        self.add_media(form)
-
         """override values_and_attributes to handle initial displayed values"""
         values, attrs = super(LazyRestrictedAutoCompletionWidget, self).values_and_attributes(form, field)
         assert len(values) == 1, "multiple selection is not supported yet by LazyWidget"
@@ -628,123 +759,77 @@
         return values, attrs
 
     def display_value_for(self, form, value):
-        entity =form._cw.entity_from_eid(value)
+        entity = form._cw.entity_from_eid(value)
         return entity.view('combobox')
 
 
 class AddComboBoxWidget(Select):
-    def values_and_attributes(self, form, field):
-        values, attrs = super(AddComboBoxWidget, self).values_and_attributes(form, field)
-        init_ajax_attributes(self.attrs, 'AddComboBox')
+    def attributes(self, form, field):
+        attrs = super(AddComboBoxWidget, self).attributes(form, field)
+        init_ajax_attributes(attrs, 'AddComboBox')
         # XXX entity form specific
         entity = form.edited_entity
         attrs['cubicweb:etype_to'] = entity.e_schema
         etype_from = entity.e_schema.subjrels[field.name].objects(entity.e_schema)[0]
         attrs['cubicweb:etype_from'] = etype_from
-        return values, attrs
+        return attrs
 
-    def render(self, form, field, renderer):
-        return super(AddComboBoxWidget, self).render(form, field, renderer) + u'''
+    def _render(self, form, field, renderer):
+        return super(AddComboBoxWidget, self)._render(form, field, renderer) + u'''
 <div id="newvalue">
   <input type="text" id="newopt" />
   <a href="javascript:noop()" id="add_newopt">&#160;</a></div>
 '''
 
-# buttons ######################################################################
+# more widgets #################################################################
+
+class IntervalWidget(FieldWidget):
+    """Custom widget to display an interval composed by 2 fields. This widget is
+    expected to be used with a :class:`CompoundField` containing the two actual
+    fields.
 
-class Button(Input):
-    """<input type='button'>, base class for global form buttons
+    Exemple usage::
 
-    note label is a msgid which will be translated at form generation time, you
-    should not give an already translated string.
+      class MyForm(FieldsForm):
+         price = CompoundField(fields=(IntField(name='minprice'),
+                                       IntField(name='maxprice')),
+                               label=_('price'),
+                               widget=IntervalWidget())
     """
-    type = 'button'
-    def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
-                 setdomid=None, settabindex=None,
-                 name='', value='', onclick=None, cwaction=None):
-        super(Button, self).__init__(attrs, setdomid, settabindex)
-        if isinstance(label, tuple):
-            self.label = label[0]
-            self.icon = label[1]
-        else:
-            self.label = label
-            self.icon = None
-        self.name = name
-        self.value = ''
-        self.onclick = onclick
-        self.cwaction = cwaction
-        self.attrs.setdefault('class', 'validateButton')
-
-    def render(self, form, field=None, renderer=None):
-        label = form._cw._(self.label)
-        attrs = self.attrs.copy()
-        if self.cwaction:
-            assert self.onclick is None
-            attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
-                self.cwaction, self.label, form.domid)
-        elif self.onclick:
-            attrs['onclick'] = self.onclick
-        if self.name:
-            attrs['name'] = self.name
-            if self.setdomid:
-                attrs['id'] = self.name
-        if self.settabindex and not 'tabindex' in attrs:
-            attrs['tabindex'] = form._cw.next_tabindex()
-        if self.icon:
-            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
-        else:
-            img = u''
-        return tags.button(img + xml_escape(label), escapecontent=False,
-                           value=label, type=self.type, **attrs)
+    def _render(self, form, field, renderer):
+        actual_fields = field.fields
+        assert len(actual_fields) == 2
+        return u'<div>%s %s %s %s</div>' % (
+            form._cw._('from_interval_start'),
+            actual_fields[0].render(form, renderer),
+            form._cw._('to_interval_end'),
+            actual_fields[1].render(form, renderer),
+            )
 
 
-class SubmitButton(Button):
-    """<input type='submit'>, main button to submit a form"""
-    type = 'submit'
-
-
-class ResetButton(Button):
-    """<input type='reset'>, main button to reset a form.
-    You usually don't want this.
+class HorizontalLayoutWidget(FieldWidget):
+    """Custom widget to display a set of fields grouped together horizontally in
+    a form. See `IntervalWidget` for example usage.
     """
-    type = 'reset'
+    def _render(self, form, field, renderer):
+        if self.attrs.get('display_label', True):
+            subst = self.attrs.get('label_input_substitution', '%(label)s %(input)s')
+            fields = [subst % {'label': renderer.render_label(form, f),
+                              'input': f.render(form, renderer)}
+                      for f in field.subfields(form)]
+        else:
+            fields = [f.render(form, renderer) for f in field.subfields(form)]
+        return u'<div>%s</div>' % ' '.join(fields)
 
 
-class ImgButton(object):
-    """<img> wrapped into a <a> tag with href triggering something (usually a
-    javascript call)
-
-    note label is a msgid which will be translated at form generation time, you
-    should not give an already translated string.
-    """
-    def __init__(self, domid, href, label, imgressource):
-        self.domid = domid
-        self.href = href
-        self.imgressource = imgressource
-        self.label = label
+class EditableURLWidget(FieldWidget):
+    """Custom widget to edit separatly an url path / query string (used by
+    default for the `path` attribute of `Bookmark` entities).
 
-    def render(self, form, field=None, renderer=None):
-        label = form._cw._(self.label)
-        imgsrc = form._cw.external_resource(self.imgressource)
-        return '<a id="%(domid)s" href="%(href)s">'\
-               '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
-            'label': label, 'imgsrc': imgsrc,
-            'domid': self.domid, 'href': self.href}
-
-
-# more widgets #################################################################
-
-class EditableURLWidget(FieldWidget):
-    """custom widget to edit separatly an url path / query string (used by
-    default for Bookmark.path for instance), dealing with url quoting nicely
-    (eg user edit the unquoted value).
+    It deals with url quoting nicely so that the user edit the unquoted value.
     """
 
     def _render(self, form, field, renderer):
-        """render the widget for the given `field` of `form`.
-
-        Generate one <input> tag for each field's value
-        """
         assert self.suffix is None, 'not supported'
         req = form._cw
         pathqname = field.input_name(form, 'path')
@@ -813,3 +898,83 @@
         if not values:
             return path
         return u'%s?%s' % (path, req.build_url_params(**values))
+
+
+# form controls ######################################################################
+
+class Button(Input):
+    """Simple <input type='button'>, base class for global form buttons.
+
+    Note that `label` is a msgid which will be translated at form generation
+    time, you should not give an already translated string.
+    """
+    type = 'button'
+    def __init__(self, label=stdmsgs.BUTTON_OK, attrs=None,
+                 setdomid=None, settabindex=None,
+                 name='', value='', onclick=None, cwaction=None):
+        super(Button, self).__init__(attrs, setdomid, settabindex)
+        if isinstance(label, tuple):
+            self.label = label[0]
+            self.icon = label[1]
+        else:
+            self.label = label
+            self.icon = None
+        self.name = name
+        self.value = ''
+        self.onclick = onclick
+        self.cwaction = cwaction
+        self.attrs.setdefault('class', 'validateButton')
+
+    def render(self, form, field=None, renderer=None):
+        label = form._cw._(self.label)
+        attrs = self.attrs.copy()
+        if self.cwaction:
+            assert self.onclick is None
+            attrs['onclick'] = "postForm('__action_%s', \'%s\', \'%s\')" % (
+                self.cwaction, self.label, form.domid)
+        elif self.onclick:
+            attrs['onclick'] = self.onclick
+        if self.name:
+            attrs['name'] = self.name
+            if self.setdomid:
+                attrs['id'] = self.name
+        if self.settabindex and not 'tabindex' in attrs:
+            attrs['tabindex'] = form._cw.next_tabindex()
+        if self.icon:
+            img = tags.img(src=form._cw.external_resource(self.icon), alt=self.icon)
+        else:
+            img = u''
+        return tags.button(img + xml_escape(label), escapecontent=False,
+                           value=label, type=self.type, **attrs)
+
+
+class SubmitButton(Button):
+    """Simple <input type='submit'>, main button to submit a form"""
+    type = 'submit'
+
+
+class ResetButton(Button):
+    """Simple <input type='reset'>, main button to reset a form. You usually
+    don't want to use this.
+    """
+    type = 'reset'
+
+
+class ImgButton(object):
+    """Simple <img> wrapped into a <a> tag with href triggering something (usually a
+    javascript call).
+    """
+    def __init__(self, domid, href, label, imgressource):
+        self.domid = domid
+        self.href = href
+        self.imgressource = imgressource
+        self.label = label
+
+    def render(self, form, field=None, renderer=None):
+        label = form._cw._(self.label)
+        imgsrc = form._cw.external_resource(self.imgressource)
+        return '<a id="%(domid)s" href="%(href)s">'\
+               '<img src="%(imgsrc)s" alt="%(label)s"/>%(label)s</a>' % {
+            'label': label, 'imgsrc': imgsrc,
+            'domid': self.domid, 'href': self.href}
+
--- a/web/request.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/request.py	Fri Apr 23 12:42:53 2010 +0200
@@ -16,7 +16,10 @@
 from urlparse import urlsplit
 from itertools import count
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from rql.utils import rqlvar_maker
 
--- a/web/test/unittest_views_basecontrollers.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/test/unittest_views_basecontrollers.py	Fri Apr 23 12:42:53 2010 +0200
@@ -5,7 +5,10 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from logilab.common.testlib import unittest_main, mock_object
 
@@ -550,7 +553,7 @@
 #         rql = 'Any T,N WHERE T is Tag, T name N'
 #         ctrl = self.ctrl(self.request(mode='json', rql=rql, pageid='123'))
 #         self.assertEquals(ctrl.publish(),
-#                           simplejson.dumps(self.execute(rql).rows))
+#                           json.dumps(self.execute(rql).rows))
 
     def test_remote_add_existing_tag(self):
         self.remote_call('tag_entity', self.john.eid, ['python'])
@@ -631,14 +634,14 @@
     # silly tests
     def test_external_resource(self):
         self.assertEquals(self.remote_call('external_resource', 'RSS_LOGO')[0],
-                          simplejson.dumps(self.request().external_resource('RSS_LOGO')))
+                          json.dumps(self.request().external_resource('RSS_LOGO')))
     def test_i18n(self):
         self.assertEquals(self.remote_call('i18n', ['bimboom'])[0],
-                          simplejson.dumps(['bimboom']))
+                          json.dumps(['bimboom']))
 
     def test_format_date(self):
         self.assertEquals(self.remote_call('format_date', '2007-01-01 12:00:00')[0],
-                          simplejson.dumps('2007/01/01'))
+                          json.dumps('2007/01/01'))
 
 
 
--- a/web/test/unittest_views_baseviews.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/test/unittest_views_baseviews.py	Fri Apr 23 12:42:53 2010 +0200
@@ -5,7 +5,10 @@
 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
 :license: GNU Lesser General Public License, v2.1 - http://www.gnu.org/licenses
 """
-from simplejson import loads
+try:
+    from json import loads
+except ImportError:
+    from simplejson import loads
 
 from logilab.common.testlib import unittest_main
 from logilab.mtconverter import html_unescape
--- a/web/views/autoform.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/autoform.py	Fri Apr 23 12:42:53 2010 +0200
@@ -1,9 +1,107 @@
-"""The automatic entity form.
+# organization: Logilab
+# copyright: 2001-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 automatic entity form
+-------------------------
+
+.. autodocstring:: cubicweb.web.views.autoform::AutomaticEntityForm
+
+Configuration through uicfg
+```````````````````````````
+
+It is possible to manage which and how an entity's attributes and relations
+will be edited in the various context where the automatic entity form is used
+by using proper uicfg tags.
+
+The details of the uicfg syntax can be found in the :ref:`uicfg` chapter.
+
+Possible relation tags that apply to entity forms are detailled below.
+They are all in the :mod:`cubicweb.web.uicfg` module.
+
+Attributes/relations display location
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+``autoform_section`` specifies where to display a relation in form for a given
+form type.  :meth:`tag_attribute`, :meth:`tag_subject_of` and
+:meth:`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 (e.g. the one you get when creating or editing an
+  entity)
+
+* 'inlined', the form for an entity inlined into another form
+
+* 'muledit', the table form when editing multiple entities of the same type
+
+
+section may be one of:
+
+* 'hidden', don't display (not even in an hidden input, right?)
+
+* '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 usable for attributes)
+
+* 'inlined', display target entity of the relation into an inlined form
+  (available in main form only, and not for attributes)
 
-:organization: Logilab
-:copyright: 2001-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
+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 class to use for a relation
+or attribute. You can put either a field class or instance as value (put a class
+whenether it's possible).
+
+.. Warning::
+
+   `autoform_field_kwargs` should usually be used instead of
+   `autoform_field`. If you put a field instance into `autoform_field`,
+   `autoform_field_kwargs` values for this relation will be ignored.
+
+
+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 dictionary as arguments, that will be given to the field's contructor.
+
+You can then put in that dictionary any arguments supported by the field
+class. For instance:
+
+.. 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
+   autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'),
+                                        {'sort': False,
+                                         'choices': ticket_done_in_choices})
+
+   # Force usage of a TextInput widget for the expression attribute of
+   # RQLExpression entities
+   autoform_field_kwargs.tag_attribute(('RQLExpression', 'expression'),
+                                       {'widget': fw.TextInput})
+
+
+
+Overriding permissions
+^^^^^^^^^^^^^^^^^^^^^^
+
+The `autoform_permissions_overrides` rtag provides a way to by-pass security
+checking for dark-corner case where it can't be verified properly.
+
+
+.. More about inlined forms
+.. Controlling the generic relation fields
 """
 
 __docformat__ = "restructuredtext en"
@@ -11,7 +109,10 @@
 
 from warnings import warn
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import iclassmethod, cached
@@ -512,16 +613,13 @@
 # The automatic entity form ####################################################
 
 class AutomaticEntityForm(forms.EntityFieldsForm):
-    """base automatic form to edit any entity.
-
-    Designed to be fully generated from schema but highly configurable through:
+    """AutomaticEntityForm is an automagic form to edit any entity. It is
+    designed to be fully generated from schema but highly configurable through
+    :ref:`uicfg`.
 
-    * uicfg (autoform_* relation tags)
-    * various standard form parameters
-    * overriding
-
-    You can also easily customise it by adding/removing fields in
-    AutomaticEntityForm instances or by inheriting from it.
+    Of course, as for other forms, you can also customise it by specifying
+    various standard form parameters on selection, overriding, or
+    adding/removing fields in a selected instances.
     """
     __regid__ = 'edition'
 
--- a/web/views/basecontrollers.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/basecontrollers.py	Fri Apr 23 12:42:53 2010 +0200
@@ -10,7 +10,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from logilab.common.decorators import cached
 from logilab.common.date import strptime
@@ -32,7 +35,7 @@
     HAS_SEARCH_RESTRICTION = False
 
 def jsonize(func):
-    """decorator to sets correct content_type and calls `simplejson.dumps` on
+    """decorator to sets correct content_type and calls `json.dumps` on
     results
     """
     def wrapper(self, *args, **kwargs):
@@ -237,7 +240,7 @@
         errback = str(self._cw.form.get('__onfailure', 'null'))
         cbargs = str(self._cw.form.get('__cbargs', 'null'))
         self._cw.set_content_type('text/html')
-        jsargs = simplejson.dumps((status, args, entity), cls=CubicWebJsonEncoder)
+        jsargs = json.dumps((status, args, entity), cls=CubicWebJsonEncoder)
         return """<script type="text/javascript">
  window.parent.handleFormValidationResponse('%s', %s, %s, %s, %s);
 </script>""" %  (domid, callback, errback, jsargs, cbargs)
@@ -277,7 +280,7 @@
         if not isinstance(args, (list, tuple)):
             args = (args,)
         try:
-            args = [simplejson.loads(arg) for arg in args]
+            args = [json.loads(arg) for arg in args]
         except ValueError, exc:
             self.exception('error while decoding json arguments for js_%s: %s', fname, args, exc)
             raise RemoteCallFailed(repr(exc))
@@ -441,7 +444,7 @@
         entity = self._cw.entity_from_eid(int(self._cw.form['eid']))
         # note: default is reserved in js land
         args['default'] = self._cw.form['default_value']
-        args['reload'] = simplejson.loads(args['reload'])
+        args['reload'] = json.loads(args['reload'])
         rset = req.eid_rset(int(self._cw.form['eid']))
         view = req.vreg['views'].select('doreledit', req, rset=rset, rtype=args['rtype'])
         stream = view.set_stream()
@@ -577,7 +580,7 @@
         rql = 'SET F %(rel)s T WHERE F eid %(eid_to)s, T eid %(eid_from)s' % {'rel' : rel, 'eid_to' : eid_to, 'eid_from' : eid_from}
         return eid_from
 
-
+# XXX move to massmailing
 class SendMailController(Controller):
     __regid__ = 'sendmail'
     __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject')
@@ -585,13 +588,12 @@
     def recipients(self):
         """returns an iterator on email's recipients as entities"""
         eids = self._cw.form['recipient']
-        # make sure we have a list even though only one recipient was specified
+        # eids may be a string if only one recipient was specified
         if isinstance(eids, basestring):
-            eids = (eids,)
-        rql = 'Any X WHERE X eid in (%s)' % (','.join(eids))
-        rset = self._cw.execute(rql)
-        for entity in rset.entities():
-            yield entity
+            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)))
+        return rset.entities()
 
     def sendmail(self, recipient, subject, body):
         msg = format_mail({'email' : self._cw.user.get_email(),
@@ -610,7 +612,6 @@
         for recipient in self.recipients():
             text = body % recipient.as_email_context()
             self.sendmail(recipient.get_email(), subject, text)
-        #breadcrumbs = self._cw.session.data.get('breadcrumbs', None)
         url = self._cw.build_url(__message=self._cw._('emails successfully sent'))
         raise Redirect(url)
 
--- a/web/views/editforms.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/editforms.py	Fri Apr 23 12:42:53 2010 +0200
@@ -11,7 +11,10 @@
 
 from copy import copy
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
 from logilab.common.decorators import cached
--- a/web/views/facets.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/facets.py	Fri Apr 23 12:42:53 2010 +0200
@@ -7,7 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
 
--- a/web/views/formrenderers.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/formrenderers.py	Fri Apr 23 12:42:53 2010 +0200
@@ -1,9 +1,22 @@
-"""form renderers, responsible to layout a form to html
+# 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
+"""
+Renderers
+---------
 
-: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
+.. Note::
+   Form renderers are responsible to layout a form to HTML.
+
+Here are the base renderers available:
+
+.. autoclass:: cubicweb.web.views.formrenderers.FormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer
+.. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer
+
 """
 __docformat__ = "restructuredtext en"
 
@@ -12,7 +25,10 @@
 from logilab.common import dictattr
 from logilab.mtconverter import xml_escape
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from cubicweb import tags
 from cubicweb.appobject import AppObject
@@ -37,7 +53,7 @@
 
 
 class FormRenderer(AppObject):
-    """basic renderer displaying fields in a two columns table label | value
+    """This is the 'default' renderer, displaying fields in a two columns table:
 
     +--------------+--------------+
     | field1 label | field1 input |
@@ -256,7 +272,7 @@
 
 
 class HTableFormRenderer(FormRenderer):
-    """display fields horizontally in a table
+    """The 'htable' form renderer display fields horizontally in a table:
 
     +--------------+--------------+---------+
     | field1 label | field2 label |         |
@@ -300,7 +316,13 @@
 
 
 class EntityCompositeFormRenderer(FormRenderer):
-    """specific renderer for multiple entities edition form (muledit)"""
+    """This is a specific renderer for the multiple entities edition form
+    ('muledit').
+
+    Each entity form will be displayed in row off a table, with a check box for
+    each entities to indicate which ones are edited. Those checkboxes should be
+    automatically updated when something is edited.
+    """
     __regid__ = 'composite'
 
     _main_display_fields = None
@@ -359,7 +381,11 @@
 
 
 class EntityFormRenderer(BaseFormRenderer):
-    """specific renderer for entity edition form (edition)"""
+    """This is the 'default' renderer for entity's form.
+
+    You can still use form_renderer_id = 'base' if you want base FormRenderer
+    layout even when selected for an entity.
+    """
     __regid__ = 'default'
     # needs some additional points in some case (XXX explain cases)
     __select__ = implements('Any') & yes()
@@ -396,8 +422,8 @@
 
 
 class EntityInlinedFormRenderer(EntityFormRenderer):
-    """specific renderer for entity inlined edition form
-    (inline-[creation|edition])
+    """This is a specific renderer for entity's form inlined into another
+    entity's form.
     """
     __regid__ = 'inline'
 
--- a/web/views/forms.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/forms.py	Fri Apr 23 12:42:53 2010 +0200
@@ -1,9 +1,32 @@
-"""some base form classes for CubicWeb web client
+# organization: Logilab
+# copyright: 2001-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
+"""
+Base form classes
+-----------------
+
+.. Note:
+
+   Form is the glue that bind a context to a set of fields, and is rendered
+   using a form renderer. No display is actually done here, though you'll find
+   some attributes of form that are used to control the rendering process.
 
-:organization: Logilab
-:copyright: 2001-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
+Besides the automagic form we'll see later, they are barely two form
+classes in |cubicweb|:
+
+.. autoclass:: cubicweb.web.views.forms.FieldsForm
+.. autoclass:: cubicweb.web.views.forms.EntityFieldsForm
+
+As you have probably guessed, choosing between them is easy. Simply ask you the
+question 'I am editing an entity or not?'. If the answer is yes, use
+:class:`EntityFieldsForm`, else use :class:`FieldsForm`.
+
+Actually there exists a third form class:
+
+.. autoclass:: cubicweb.web.views.forms.CompositeForm
+
+but you'll use this one rarely.
 """
 __docformat__ = "restructuredtext en"
 
@@ -16,43 +39,77 @@
 from cubicweb import typed_eid
 from cubicweb.selectors import non_final_entity, match_kwargs, one_line_rset
 from cubicweb.web import uicfg, form, formwidgets as fwdgs
-from cubicweb.web.formfields import StringField, relvoc_unrelated, guess_field
+from cubicweb.web.formfields import relvoc_unrelated, guess_field
 
 
 class FieldsForm(form.Form):
-    """base class for fields based forms.
+    """This is the base class for fields based forms.
+
+    **Attributes**
 
     The following attributes may be either set on subclasses or given on
     form selection to customize the generated form:
 
-    * `needs_js`: sequence of javascript files that should be added to handle
-      this form (through `req.add_js`)
+    :attr:`needs_js`
+      sequence of javascript files that should be added to handle this form
+      (through :meth:`~cubicweb.web.request.Request.add_js`)
 
-    * `needs_css`: sequence of css files that should be added to handle this
-      form (through `req.add_css`)
+    :attr:`needs_css`
+      sequence of css files that should be added to handle this form (through
+      :meth:`~cubicweb.web.request.Request.add_css`)
+
+    :attr:`domid`
+      value for the "id" attribute of the <form> tag
+
+    :attr:`action`
+      value for the "action" attribute of the <form> tag
 
-    * `domid`: value for the "id" attribute of the <form> tag
+    :attr:`onsubmit`
+      value for the "onsubmit" attribute of the <form> tag
 
-    * `action`: value for the "action" attribute of the <form> tag
+    :attr:`cssclass`
+      value for the "class" attribute of the <form> tag
 
-    * `onsubmit`: value for the "onsubmit" attribute of the <form> tag
+    :attr:`cssstyle`
+      value for the "style" attribute of the <form> tag
 
-    * `cssclass`: value for the "class" attribute of the <form> tag
+    :attr:`cwtarget`
+      value for the "cubicweb:target" attribute of the <form> tag
+
+    :attr:`redirect_path`
+      relative to redirect to after submitting the form
 
-    * `cssstyle`: value for the "style" attribute of the <form> tag
+    :attr:`copy_nav_params`
+      flag telling if navigation parameters should be copied back in hidden
+      inputs
+
+    :attr:`form_buttons`
+      sequence of form control (:class:`~cubicweb.web.formwidgets.Button`
+      widgets instances)
 
-    * `cwtarget`: value for the "cubicweb:target" attribute of the <form> tag
+    :attr:`form_renderer_id`
+      identifier of the form renderer to use to render the form
 
-    * `redirect_path`: relative to redirect to after submitting the form
+    :attr:`fieldsets_in_order`
+      sequence of fieldset names , to control order
+
+    **Generic methods**
 
-    * `copy_nav_params`: flag telling if navigation paramenters should be copied
-      back in hidden input
+    .. automethod:: cubicweb.web.form.Form.field_by_name(name, role=None)
+    .. automethod:: cubicweb.web.form.Form.fields_by_name(name, role=None)
+
+    **Form construction methods**
 
-    * `form_buttons`:  form buttons sequence (button widgets instances)
+    .. automethod:: cubicweb.web.form.Form.remove_field(field)
+    .. automethod:: cubicweb.web.form.Form.append_field(field)
+    .. automethod:: cubicweb.web.form.Form.insert_field_before(field, name, role=None)
+    .. automethod:: cubicweb.web.form.Form.insert_field_after(field, name, role=None)
+    .. automethod:: cubicweb.web.form.Form.add_hidden(name, value=None, **kwargs)
 
-    * `form_renderer_id`: id of the form renderer to use to render the form
+    **Form rendering methods**
 
-    * `fieldsets_in_order`: fieldset name sequence, to control order
+    .. automethod:: cubicweb.web.views.forms.FieldsForm.render
+
     """
     __regid__ = 'base'
 
@@ -75,18 +132,6 @@
         """true if the form needs enctype=multipart/form-data"""
         return any(field.needs_multipart for field in self.fields)
 
-    def add_hidden(self, name, value=None, **kwargs):
-        """add an hidden field to the form"""
-        kwargs.setdefault('ignore_req_params', True)
-        kwargs.setdefault('widget', fwdgs.HiddenInput)
-        field = StringField(name=name, value=value, **kwargs)
-        if 'id' in kwargs:
-            # by default, hidden input don't set id attribute. If one is
-            # explicitly specified, ensure it will be set
-            field.widget.setdomid = True
-        self.append_field(field)
-        return field
-
     def add_media(self):
         """adds media (CSS & JS) required by this widget"""
         if self.needs_js:
@@ -95,8 +140,16 @@
             self._cw.add_css(self.needs_css)
 
     def render(self, formvalues=None, rendervalues=None, renderer=None, **kwargs):
-        """render this form, using the renderer given in args or the default
-        FormRenderer()
+        """Render this form, using the `renderer` given as argument or the
+        default according to :attr:`form_renderer_id`. The rendered form is
+        returned as an unicode string.
+
+        `formvalues` is an optional dictionary containing values that will be
+        considered as field's value.
+
+        Extra keyword arguments will be given to renderer's :meth:`render` method.
+
+        `rendervalues` is deprecated.
         """
         if rendervalues is not None:
             warn('[3.6] rendervalues argument is deprecated, all named arguments will be given instead',
@@ -148,6 +201,11 @@
 _AFF_KWARGS = uicfg.autoform_field_kwargs
 
 class EntityFieldsForm(FieldsForm):
+    """This class is designed for forms used to edit some entities. It should
+    handle for you all the underlying stuff necessary to properly work with the
+    generic :class:`~cubicweb.web.views.editcontroller.EditController`.
+    """
+
     __regid__ = 'base'
     __select__ = (match_kwargs('entity')
                   | (one_line_rset() & non_final_entity()))
@@ -267,7 +325,6 @@
 
 
 class CompositeFormMixIn(object):
-    """form composed of sub-forms"""
     __regid__ = 'composite'
     form_renderer_id = __regid__
 
@@ -287,7 +344,9 @@
 
 
 class CompositeForm(CompositeFormMixIn, FieldsForm):
-    pass
+    """Form composed of sub-forms. Typical usage is edition of multiple entities
+    at once.
+    """
 
 class CompositeEntityForm(CompositeFormMixIn, EntityFieldsForm):
     pass # XXX why is this class necessary?
--- a/web/views/igeocodable.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/igeocodable.py	Fri Apr 23 12:42:53 2010 +0200
@@ -7,7 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from cubicweb.interfaces import IGeocodable
 from cubicweb.view import EntityView
@@ -39,7 +42,7 @@
             'center': center,
             'markers': markers,
             }
-        self.w(simplejson.dumps(geodata))
+        self.w(json.dumps(geodata))
 
     def build_marker_data(self, row, extraparams):
         entity = self.cw_rset.get_entity(row, 0)
--- a/web/views/massmailing.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/massmailing.py	Fri Apr 23 12:42:53 2010 +0200
@@ -11,7 +11,7 @@
 import operator
 
 from cubicweb.interfaces import IEmailable
-from cubicweb.selectors import implements, match_user_groups
+from cubicweb.selectors import implements, authenticated_user
 from cubicweb.view import EntityView
 from cubicweb.web import stdmsgs, action, form, formfields as ff
 from cubicweb.web.formwidgets import CheckBox, TextInput, AjaxWidget, ImgButton
@@ -22,7 +22,7 @@
     __regid__ = 'sendemail'
     # XXX should check email is set as well
     __select__ = (action.Action.__select__ & implements(IEmailable)
-                  & match_user_groups('managers', 'users'))
+                  & authenticated_user())
 
     title = _('send email')
     category = 'mainactions'
@@ -42,6 +42,11 @@
 class MassMailingForm(forms.FieldsForm):
     __regid__ = 'massmailing'
 
+    needs_js = ('cubicweb.widgets.js', 'cubicweb.massmailing.js')
+    needs_css = ('cubicweb.mailform.css')
+    domid = 'sendmail'
+    action = 'sendmail'
+
     sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}),
                             label=_('From:'),
                             value=lambda f: '%s <%s>' % (f._cw.user.dc_title(), f._cw.user.get_email()))
@@ -81,7 +86,6 @@
 
 class MassMailingFormRenderer(formrenderers.FormRenderer):
     __regid__ = 'massmailing'
-    button_bar_class = u'toolbar'
 
     def _render_fields(self, fields, w, form):
         w(u'<table class="headersform">')
@@ -115,15 +119,12 @@
     def render_buttons(self, w, form):
         pass
 
+
 class MassMailingFormView(form.FormViewMixIn, EntityView):
     __regid__ = 'massmailing'
-    __select__ = implements(IEmailable) & match_user_groups('managers', 'users')
+    __select__ = implements(IEmailable) & authenticated_user()
 
     def call(self):
-        req = self._cw
-        req.add_js('cubicweb.widgets.js', 'cubicweb.massmailing.js')
-        req.add_css('cubicweb.mailform.css')
-        from_addr = '%s <%s>' % (req.user.dc_title(), req.user.get_email())
-        form = self._cw.vreg['forms'].select('massmailing', self._cw, rset=self.cw_rset,
-                                             action='sendmail', domid='sendmail')
+        form = self._cw.vreg['forms'].select('massmailing', self._cw,
+                                             rset=self.cw_rset)
         self.w(form.render())
--- a/web/views/plots.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/plots.py	Fri Apr 23 12:42:53 2010 +0200
@@ -7,7 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.common.date import datetime2ticks
 from logilab.mtconverter import xml_escape
--- a/web/views/tableview.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/tableview.py	Fri Apr 23 12:42:53 2010 +0200
@@ -8,7 +8,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-from simplejson import dumps
+try:
+    from json import dumps
+except ImportError:
+    from simplejson import dumps
 
 from logilab.mtconverter import xml_escape
 
--- a/web/views/tabs.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/tabs.py	Fri Apr 23 12:42:53 2010 +0200
@@ -47,6 +47,8 @@
         if show_spinbox:
             w(u'<img src="data/loading.gif" id="%s-hole" alt="%s"/>'
               % (tabid or vid, self._cw._('(loading ...)')))
+        else:
+            w(u'<div id="%s-hole"></div>' % (tabid or vid))
         w(u'<noscript><p><a class="style: hidden" id="seo-%s" href="%s">%s</a></p></noscript>'
           % (tabid or vid, xml_escape(self._cw.build_url(**urlparams)), xml_escape('%s (%s)') %
              (tabid or vid, self._cw._('follow this link if javascript is deactivated'))))
--- a/web/views/timeline.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/timeline.py	Fri Apr 23 12:42:53 2010 +0200
@@ -9,7 +9,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-import simplejson
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from logilab.mtconverter import xml_escape
 
@@ -40,7 +43,7 @@
                 events.append(event)
         timeline_data = {'dateTimeFormat': self.date_fmt,
                          'events': events}
-        self.w(simplejson.dumps(timeline_data))
+        self.w(json.dumps(timeline_data))
 
     # FIXME: those properties should be defined by the entity class
     def onclick_url(self, entity):
--- a/web/views/treeview.py	Fri Apr 23 11:10:30 2010 +0200
+++ b/web/views/treeview.py	Fri Apr 23 12:42:53 2010 +0200
@@ -7,7 +7,10 @@
 """
 __docformat__ = "restructuredtext en"
 
-import simplejson as json
+try:
+    import json
+except ImportError:
+    import simplejson as json
 
 from logilab.mtconverter import xml_escape
 from cubicweb.utils import make_uid