# HG changeset patch # User Sylvain Thénault # Date 1272019373 -7200 # Node ID b6e250dd7a7d9240c4fd7e8807a5d8ed59302e1b # Parent cb5dfea92285ecb72d6ff60633ae604085b3e6d8# Parent b619531ddbd28707a5cfbaf65af72b63d0808a96 backport stable diff -r cb5dfea92285 -r b6e250dd7a7d devtools/devctl.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): diff -r cb5dfea92285 -r b6e250dd7a7d devtools/testlib.py --- 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) diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/admin/index.rst --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/admin/migration.rst --- /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 + diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/datamodel/definition.rst --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/devweb/form.rst --- 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'') + for field in fields: + if field.name == 'mailbody': + w(u'
') + w(u'
') + w(u'') + w(u'
') + w(u'
') + w(field.render(form, self)) + w(u'
') + else: + w(u'') + w(u'%s' % self.render_label(form, field)) + w(u'') + w(field.render(form, self)) + w(u'') + def render_buttons(self, w, form): + pass + +We simply override the `_render_fields` and `render_buttons` method of the base form renderer +to arrange fields as we desire it: here we'll have first a two columns table with label and +value of the sender, recipients and subject field (form order respected), then form controls, +then a div containing the textarea for the email's content. + +To bind this renderer to our form, we should add to our form definition above: + +.. sourcecode:: python + + form_renderer_id = 'massmailing' + + +.. Example of entity fields form diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/devweb/publisher.rst --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/devweb/views/autoform.rst --- 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(, tag) - uicfg.autoform_section.tag_object_of(, tag) - uicfg.autoform_field.tag_attribute(, tag) - -The details of the uicfg syntax can be found in the :ref:`uicfg` -chapter. - -Possible tags are detailled below - -Automatic form configuration -```````````````````````````` - -Attributes/relations display location -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -``uicfg.autoform_section`` specifies where to display a relation in -creation/edition entity form for a given form type. ``tag_attribute``, -``tag_subject_of`` and ``tag_object_of`` methods for this relation tag expect -two arguments additionally to the relation key: a ``formtype`` and a -``section``. - -``formtype`` may be one of: - -* ``main``, the main entity form (via the modify action) -* ``inlined``, the form for an entity inlined into another form -* ``muledit``, the table form to edit multiple entities - -section may be one of: - -* ``hidden``, don't display - -* ``attributes``, display in the attributes section - -* ``relations``, display in the relations section, using the generic relation - selector combobox (available in main form only, and not for attribute - relation) - -* ``inlined``, display target entity of the relation in an inlined form - (available in main form only, and not for attribute relation) - -* ``metadata``, display in a special metadata form (NOT YET IMPLEMENTED, subject - to changes) - -By default, mandatory relations are displayed in the ``attributes`` section, -others in ``relations`` section. - -Change default fields -^^^^^^^^^^^^^^^^^^^^^ - -Use ``autoform_field`` to replace the default field type of an attribute. - -.. warning:: - - ``autoform_field_kwargs`` should usually be used instead of - ``autoform_field``. Do not use both methods for the same relation! - - -Customize field options -^^^^^^^^^^^^^^^^^^^^^^^ - -In order to customize field options (see :class:`cubicweb.web.formfields.Field` -for a detailed list of options), use ``autoform_field_kwargs``. This rtag takes -a relation triplet and a dictionary as arguments. - -.. sourcecode:: python - - # Change the content of the combobox - # here ``ticket_done_in_choices`` is a function which returns a list of - # elements to populate the combobox - uicfg.autoform_field_kwargs.tag_subject_of(('Ticket', 'done_in', '*'), {'sort': False, - 'choices': ticket_done_in_choices}) - - - -Overriding permissions -^^^^^^^^^^^^^^^^^^^^^^ - -``autoform_permissions_overrides`` provides a way to by-pass security checking -for dark-corner case where it can't be verified properly. XXX documents. diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/devweb/views/index.rst diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/development/devweb/views/primary.rst --- 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') diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/standard_theme/static/lglb-sphinx-doc.css --- 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 { diff -r cb5dfea92285 -r b6e250dd7a7d doc/book/en/tutorials/advanced/index.rst --- 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... diff -r cb5dfea92285 -r b6e250dd7a7d etwist/service.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d goa/goactl.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d server/repository.py --- 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) diff -r cb5dfea92285 -r b6e250dd7a7d server/utils.py --- 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)) diff -r cb5dfea92285 -r b6e250dd7a7d test/unittest_utils.py --- 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)), diff -r cb5dfea92285 -r b6e250dd7a7d utils.py --- 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, diff -r cb5dfea92285 -r b6e250dd7a7d view.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d web/__init__.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d web/_exceptions.py --- 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""" diff -r cb5dfea92285 -r b6e250dd7a7d web/application.py --- 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: diff -r cb5dfea92285 -r b6e250dd7a7d web/component.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d web/form.py --- 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 diff -r cb5dfea92285 -r b6e250dd7a7d web/formfields.py --- 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) diff -r cb5dfea92285 -r b6e250dd7a7d web/formwidgets.py --- 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 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): - """""" + """Simple , will return an unicode string.""" type = 'text' +class PasswordSingleInput(Input): + """Simple , will return an utf-8 encoded string. + + You may prefer using the :class:`~cubicweb.web.formwidgets.PasswordInput` + widget which handles password confirmation. + """ + type = 'password' + + def process_field_data(self, form, field): + value = super(PasswordSingleInput, self).process_field_data(form, field) + if value is not None: + return value.encode('utf-8') + return value + + class PasswordInput(Input): - """ and its confirmation field (using - -confirm as name) + """ and a confirmation input. Form processing will + fail if password and confirmation differs, else it will return the password + as an utf-8 encoded string. """ type = 'password' @@ -189,45 +339,38 @@ raise ProcessFormError(form._cw._("password and confirmation don't match")) -class PasswordSingleInput(Input): - """ without a confirmation field""" - type = 'password' - - def process_field_data(self, form, field): - value = super(PasswordSingleInput, self).process_field_data(form, field) - if value is not None: - return value.encode('utf-8') - return value - - class FileInput(Input): - """""" + """Simple , will return a tuple (name, stream) where + name is the posted file name and stream a file like object containing the + posted file data. + """ type = 'file' - def values_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): - """""" + """Simple for hidden value, will return an unicode + string. + """ type = 'hidden' setdomid = False # by default, don't set id attribute on hidden input settabindex = False class ButtonInput(Input): - """ + """Simple , will return an unicode string. - if you want a global form button, look at the Button, SubmitButton, - ResetButton and ImgButton classes below. + If you want a global form button, look at the :class:`Button`, + :class:`SubmitButton`, :class:`ResetButton` and :class:`ImgButton` below. """ type = 'button' class TextArea(FieldWidget): - """