|
1 .. _webform: |
|
2 |
|
3 HTML form construction |
|
4 ---------------------- |
|
5 |
|
6 CubicWeb provides the somewhat usual form / field / widget / renderer abstraction |
|
7 to provide generic building blocks which will greatly help you in building forms |
|
8 properly integrated with CubicWeb (coherent display, error handling, etc...), |
|
9 while keeping things as flexible as possible. |
|
10 |
|
11 A ``form`` basically only holds a set of ``fields``, and has te be bound to a |
|
12 ``renderer`` which is responsible to layout them. Each field is bound to a |
|
13 ``widget`` that will be used to fill in value(s) for that field (at form |
|
14 generation time) and 'decode' (fetch and give a proper Python type to) values |
|
15 sent back by the browser. |
|
16 |
|
17 The ``field`` should be used according to the type of what you want to edit. |
|
18 E.g. if you want to edit some date, you'll have to use the |
|
19 :class:`cubicweb.web.formfields.DateField`. Then you can choose among multiple |
|
20 widgets to edit it, for instance :class:`cubicweb.web.formwidgets.TextInput` (a |
|
21 bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple |
|
22 calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery |
|
23 calendar). You can of course also write your own widget. |
|
24 |
|
25 Exploring the available forms |
|
26 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
27 |
|
28 A small excursion into a |cubicweb| shell is the quickest way to |
|
29 discover available forms (or application objects in general). |
|
30 |
|
31 .. sourcecode:: python |
|
32 |
|
33 >>> from pprint import pprint |
|
34 >>> pprint( session.vreg['forms'] ) |
|
35 {'base': [<class 'cubicweb.web.views.forms.FieldsForm'>, |
|
36 <class 'cubicweb.web.views.forms.EntityFieldsForm'>], |
|
37 'changestate': [<class 'cubicweb.web.views.workflow.ChangeStateForm'>, |
|
38 <class 'cubes.tracker.views.forms.VersionChangeStateForm'>], |
|
39 'composite': [<class 'cubicweb.web.views.forms.CompositeForm'>, |
|
40 <class 'cubicweb.web.views.forms.CompositeEntityForm'>], |
|
41 'deleteconf': [<class 'cubicweb.web.views.editforms.DeleteConfForm'>], |
|
42 'edition': [<class 'cubicweb.web.views.autoform.AutomaticEntityForm'>, |
|
43 <class 'cubicweb.web.views.workflow.TransitionEditionForm'>, |
|
44 <class 'cubicweb.web.views.workflow.StateEditionForm'>], |
|
45 'logform': [<class 'cubicweb.web.views.basetemplates.LogForm'>], |
|
46 'massmailing': [<class 'cubicweb.web.views.massmailing.MassMailingForm'>], |
|
47 'muledit': [<class 'cubicweb.web.views.editforms.TableEditForm'>], |
|
48 'sparql': [<class 'cubicweb.web.views.sparql.SparqlForm'>]} |
|
49 |
|
50 |
|
51 The two most important form families here (for all practical purposes) are `base` |
|
52 and `edition`. Most of the time one wants alterations of the |
|
53 :class:`AutomaticEntityForm` to generate custom forms to handle edition of an |
|
54 entity. |
|
55 |
|
56 The Automatic Entity Form |
|
57 ~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
58 |
|
59 .. automodule:: cubicweb.web.views.autoform |
|
60 |
|
61 Anatomy of a choices function |
|
62 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
63 |
|
64 Let's have a look at the `ticket_done_in_choices` function given to |
|
65 the `choices` parameter of the relation tag that is applied to the |
|
66 ('Ticket', 'done_in', '*') relation definition, as it is both typical |
|
67 and sophisticated enough. This is a code snippet from the `tracker`_ |
|
68 cube. |
|
69 |
|
70 .. _`tracker`: http://www.cubicweb.org/project/cubicweb-tracker |
|
71 |
|
72 The ``Ticket`` entity type can be related to a ``Project`` and a |
|
73 ``Version``, respectively through the ``concerns`` and ``done_in`` |
|
74 relations. When a user is about to edit a ticket, we want to fill the |
|
75 combo box for the ``done_in`` relation with values pertinent with |
|
76 respect to the context. The important context here is: |
|
77 |
|
78 * creation or modification (we cannot fetch values the same way in |
|
79 either case) |
|
80 |
|
81 * ``__linkto`` url parameter given in a creation context |
|
82 |
|
83 .. sourcecode:: python |
|
84 |
|
85 from cubicweb.web import formfields |
|
86 |
|
87 def ticket_done_in_choices(form, field): |
|
88 entity = form.edited_entity |
|
89 # first see if its specified by __linkto form parameters |
|
90 linkedto = form.linked_to[('done_in', 'subject')] |
|
91 if linkedto: |
|
92 return linkedto |
|
93 # it isn't, get initial values |
|
94 vocab = field.relvoc_init(form) |
|
95 veid = None |
|
96 # try to fetch the (already or pending) related version and project |
|
97 if not entity.has_eid(): |
|
98 peids = form.linked_to[('concerns', 'subject')] |
|
99 peid = peids and peids[0] |
|
100 else: |
|
101 peid = entity.project.eid |
|
102 veid = entity.done_in and entity.done_in[0].eid |
|
103 if peid: |
|
104 # we can complete the vocabulary with relevant values |
|
105 rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version') |
|
106 rset = form._cw.execute( |
|
107 'Any V, VN ORDERBY version_sort_value(VN) ' |
|
108 'WHERE V version_of P, P eid %(p)s, V num VN, ' |
|
109 'V in_state ST, NOT ST name "published"', {'p': peid}, 'p') |
|
110 vocab += [(v.view('combobox'), v.eid) for v in rset.entities() |
|
111 if rschema.has_perm(form._cw, 'add', toeid=v.eid) |
|
112 and v.eid != veid] |
|
113 return vocab |
|
114 |
|
115 The first thing we have to do is fetch potential values from the ``__linkto`` url |
|
116 parameter that is often found in entity creation contexts (the creation action |
|
117 provides such a parameter with a predetermined value; for instance in this case, |
|
118 ticket creation could occur in the context of a `Version` entity). The |
|
119 :class:`~cubicweb.web.formfields.RelationField` field class provides a |
|
120 :meth:`~cubicweb.web.formfields.RelationField.relvoc_linkedto` method that gets a |
|
121 list suitably filled with vocabulary values. |
|
122 |
|
123 .. sourcecode:: python |
|
124 |
|
125 linkedto = field.relvoc_linkedto(form) |
|
126 if linkedto: |
|
127 return linkedto |
|
128 |
|
129 Then, if no ``__linkto`` argument was given, we must prepare the vocabulary with |
|
130 an initial empty value (because `done_in` is not mandatory, we must allow the |
|
131 user to not select a verson) and already linked values. This is done with the |
|
132 :meth:`~cubicweb.web.formfields.RelationField.relvoc_init` method. |
|
133 |
|
134 .. sourcecode:: python |
|
135 |
|
136 vocab = field.relvoc_init(form) |
|
137 |
|
138 But then, we have to give more: if the ticket is related to a project, |
|
139 we should provide all the non published versions of this project |
|
140 (`Version` and `Project` can be related through the `version_of` |
|
141 relation). Conversely, if we do not know yet the project, it would not |
|
142 make sense to propose all existing versions as it could potentially |
|
143 lead to incoherences. Even if these will be caught by some |
|
144 RQLConstraint, it is wise not to tempt the user with error-inducing |
|
145 candidate values. |
|
146 |
|
147 The "ticket is related to a project" part must be decomposed as: |
|
148 |
|
149 * this is a new ticket which is created is the context of a project |
|
150 |
|
151 * this is an already existing ticket, linked to a project (through the |
|
152 `concerns` relation) |
|
153 |
|
154 * there is no related project (quite unlikely given the cardinality of |
|
155 the `concerns` relation, so it can only mean that we are creating a |
|
156 new ticket, and a project is about to be selected but there is no |
|
157 ``__linkto`` argument) |
|
158 |
|
159 .. note:: |
|
160 |
|
161 the last situation could happen in several ways, but of course in a |
|
162 polished application, the paths to ticket creation should be |
|
163 controlled so as to avoid a suboptimal end-user experience |
|
164 |
|
165 Hence, we try to fetch the related project. |
|
166 |
|
167 .. sourcecode:: python |
|
168 |
|
169 veid = None |
|
170 if not entity.has_eid(): |
|
171 peids = form.linked_to[('concerns', 'subject')] |
|
172 peid = peids and peids[0] |
|
173 else: |
|
174 peid = entity.project.eid |
|
175 veid = entity.done_in and entity.done_in[0].eid |
|
176 |
|
177 We distinguish between entity creation and entity modification using |
|
178 the ``Entity.has_eid()`` method, which returns `False` on creation. At |
|
179 creation time the only way to get a project is through the |
|
180 ``__linkto`` parameter. Notice that we fetch the version in which the |
|
181 ticket is `done_in` if any, for later. |
|
182 |
|
183 .. note:: |
|
184 |
|
185 the implementation above assumes that if there is a ``__linkto`` |
|
186 parameter, it is only about a project. While it makes sense most of |
|
187 the time, it is not an absolute. Depending on how an entity creation |
|
188 action action url is built, several outcomes could be possible |
|
189 there |
|
190 |
|
191 If the ticket is already linked to a project, fetching it is |
|
192 trivial. Then we add the relevant version to the initial vocabulary. |
|
193 |
|
194 .. sourcecode:: python |
|
195 |
|
196 if peid: |
|
197 rschema = form._cw.vreg.schema['done_in'].rdef('Ticket', 'Version') |
|
198 rset = form._cw.execute( |
|
199 'Any V, VN ORDERBY version_sort_value(VN) ' |
|
200 'WHERE V version_of P, P eid %(p)s, V num VN, ' |
|
201 'V in_state ST, NOT ST name "published"', {'p': peid}) |
|
202 vocab += [(v.view('combobox'), v.eid) for v in rset.entities() |
|
203 if rschema.has_perm(form._cw, 'add', toeid=v.eid) |
|
204 and v.eid != veid] |
|
205 |
|
206 .. warning:: |
|
207 |
|
208 we have to defend ourselves against lack of a project eid. Given |
|
209 the cardinality of the `concerns` relation, there *must* be a |
|
210 project, but this rule can only be enforced at validation time, |
|
211 which will happen of course only after form subsmission |
|
212 |
|
213 Here, given a project eid, we complete the vocabulary with all |
|
214 unpublished versions defined in the project (sorted by number) for |
|
215 which the current user is allowed to establish the relation. |
|
216 |
|
217 |
|
218 Building self-posted form with custom fields/widgets |
|
219 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
220 |
|
221 Sometimes you want a form that is not related to entity edition. For those, |
|
222 you'll have to handle form posting by yourself. Here is a complete example on how |
|
223 to achieve this (and more). |
|
224 |
|
225 Imagine you want a form that selects a month period. There are no proper |
|
226 field/widget to handle this in CubicWeb, so let's start by defining them: |
|
227 |
|
228 .. sourcecode:: python |
|
229 |
|
230 # let's have the whole import list at the beginning, even those necessary for |
|
231 # subsequent snippets |
|
232 from logilab.common import date |
|
233 from logilab.mtconverter import xml_escape |
|
234 from cubicweb.view import View |
|
235 from cubicweb.predicates import match_kwargs |
|
236 from cubicweb.web import RequestError, ProcessFormError |
|
237 from cubicweb.web import formfields as fields, formwidgets as wdgs |
|
238 from cubicweb.web.views import forms, calendar |
|
239 |
|
240 class MonthSelect(wdgs.Select): |
|
241 """Custom widget to display month and year. Expect value to be given as a |
|
242 date instance. |
|
243 """ |
|
244 |
|
245 def format_value(self, form, field, value): |
|
246 return u'%s/%s' % (value.year, value.month) |
|
247 |
|
248 def process_field_data(self, form, field): |
|
249 val = super(MonthSelect, self).process_field_data(form, field) |
|
250 try: |
|
251 year, month = val.split('/') |
|
252 year = int(year) |
|
253 month = int(month) |
|
254 return date.date(year, month, 1) |
|
255 except ValueError: |
|
256 raise ProcessFormError( |
|
257 form._cw._('badly formated date string %s') % val) |
|
258 |
|
259 |
|
260 class MonthPeriodField(fields.CompoundField): |
|
261 """custom field composed of two subfields, 'begin_month' and 'end_month'. |
|
262 |
|
263 It expects to be used on form that has 'mindate' and 'maxdate' in its |
|
264 extra arguments, telling the range of month to display. |
|
265 """ |
|
266 |
|
267 def __init__(self, *args, **kwargs): |
|
268 kwargs.setdefault('widget', wdgs.IntervalWidget()) |
|
269 super(MonthPeriodField, self).__init__( |
|
270 [fields.StringField(name='begin_month', |
|
271 choices=self.get_range, sort=False, |
|
272 value=self.get_mindate, |
|
273 widget=MonthSelect()), |
|
274 fields.StringField(name='end_month', |
|
275 choices=self.get_range, sort=False, |
|
276 value=self.get_maxdate, |
|
277 widget=MonthSelect())], *args, **kwargs) |
|
278 |
|
279 @staticmethod |
|
280 def get_range(form, field): |
|
281 mindate = date.todate(form.cw_extra_kwargs['mindate']) |
|
282 maxdate = date.todate(form.cw_extra_kwargs['maxdate']) |
|
283 assert mindate <= maxdate |
|
284 _ = form._cw._ |
|
285 months = [] |
|
286 while mindate <= maxdate: |
|
287 label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]), |
|
288 mindate.year) |
|
289 value = field.widget.format_value(form, field, mindate) |
|
290 months.append( (label, value) ) |
|
291 mindate = date.next_month(mindate) |
|
292 return months |
|
293 |
|
294 @staticmethod |
|
295 def get_mindate(form, field): |
|
296 return form.cw_extra_kwargs['mindate'] |
|
297 |
|
298 @staticmethod |
|
299 def get_maxdate(form, field): |
|
300 return form.cw_extra_kwargs['maxdate'] |
|
301 |
|
302 def process_posted(self, form): |
|
303 for field, value in super(MonthPeriodField, self).process_posted(form): |
|
304 if field.name == 'end_month': |
|
305 value = date.last_day(value) |
|
306 yield field, value |
|
307 |
|
308 |
|
309 Here we first define a widget that will be used to select the beginning and the |
|
310 end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as |
|
311 actual value. |
|
312 |
|
313 We then define a field that will actually hold two fields, one for the beginning |
|
314 and another for the end of the period. Each subfield uses the widget we defined |
|
315 earlier, and the outer field itself uses the standard |
|
316 :class:`IntervalWidget`. The field adds some logic: |
|
317 |
|
318 * a vocabulary generation function `get_range`, used to populate each sub-field |
|
319 |
|
320 * two 'value' functions `get_mindate` and `get_maxdate`, used to tell to |
|
321 subfields which value they should consider on form initialization |
|
322 |
|
323 * overriding of `process_posted`, called when the form is being posted, so that |
|
324 the end of the period is properly set to the last day of the month. |
|
325 |
|
326 Now, we can define a very simple form: |
|
327 |
|
328 .. sourcecode:: python |
|
329 |
|
330 class MonthPeriodSelectorForm(forms.FieldsForm): |
|
331 __regid__ = 'myform' |
|
332 __select__ = match_kwargs('mindate', 'maxdate') |
|
333 |
|
334 form_buttons = [wdgs.SubmitButton()] |
|
335 form_renderer_id = 'onerowtable' |
|
336 period = MonthPeriodField() |
|
337 |
|
338 |
|
339 where we simply add our field, set a submit button and use a very simple renderer |
|
340 (try others!). Also we specify a selector that ensures form will have arguments |
|
341 necessary to our field. |
|
342 |
|
343 Now, we need a view that will wrap the form and handle post when it occurs, |
|
344 simply displaying posted values in the page: |
|
345 |
|
346 .. sourcecode:: python |
|
347 |
|
348 class SelfPostingForm(View): |
|
349 __regid__ = 'myformview' |
|
350 |
|
351 def call(self): |
|
352 mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1) |
|
353 form = self._cw.vreg['forms'].select( |
|
354 'myform', self._cw, mindate=mindate, maxdate=maxdate, action='') |
|
355 try: |
|
356 posted = form.process_posted() |
|
357 self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted))) |
|
358 except RequestError: # no specified period asked |
|
359 pass |
|
360 form.render(w=self.w, formvalues=self._cw.form) |
|
361 |
|
362 |
|
363 Notice usage of the :meth:`process_posted` method, that will return a dictionary |
|
364 of typed values (because they have been processed by the field). In our case, when |
|
365 the form is posted you should see a dictionary with 'begin_month' and 'end_month' |
|
366 as keys with the selected dates as value (as a python `date` object). |
|
367 |
|
368 |
|
369 APIs |
|
370 ~~~~ |
|
371 |
|
372 .. automodule:: cubicweb.web.formfields |
|
373 .. automodule:: cubicweb.web.formwidgets |
|
374 .. automodule:: cubicweb.web.views.forms |
|
375 .. automodule:: cubicweb.web.views.formrenderers |
|
376 |
|
377 |