212 |
213 |
213 Here, given a project eid, we complete the vocabulary with all |
214 Here, given a project eid, we complete the vocabulary with all |
214 unpublished versions defined in the project (sorted by number) for |
215 unpublished versions defined in the project (sorted by number) for |
215 which the current user is allowed to establish the relation. |
216 which the current user is allowed to establish the relation. |
216 |
217 |
|
218 |
|
219 Building self-posted form with custom fields/widgets |
|
220 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
|
221 |
|
222 Sometimes you want a form that is not related to entity edition. For those, |
|
223 you'll have to handle form posting by yourself. Here is a complete example on how |
|
224 to achieve this (and more). |
|
225 |
|
226 Imagine you want a form that selects a month period. There are no proper |
|
227 field/widget to handle this in CubicWeb, so let's start by defining them: |
|
228 |
|
229 .. sourcecode:: python |
|
230 |
|
231 # let's have the whole import list at the beginning, even those necessary for |
|
232 # subsequent snippets |
|
233 from logilab.common import date |
|
234 from logilab.mtconverter import xml_escape |
|
235 from cubicweb.view import View |
|
236 from cubicweb.selectors import match_kwargs |
|
237 from cubicweb.web import RequestError, ProcessFormError |
|
238 from cubicweb.web import formfields as fields, formwidgets as wdgs |
|
239 from cubicweb.web.views import forms, calendar |
|
240 |
|
241 class MonthSelect(wdgs.Select): |
|
242 """Custom widget to display month and year. Expect value to be given as a |
|
243 date instance. |
|
244 """ |
|
245 |
|
246 def format_value(self, form, field, value): |
|
247 return u'%s/%s' % (value.year, value.month) |
|
248 |
|
249 def process_field_data(self, form, field): |
|
250 val = super(MonthSelect, self).process_field_data(form, field) |
|
251 try: |
|
252 year, month = val.split('/') |
|
253 year = int(year) |
|
254 month = int(month) |
|
255 return date.date(year, month, 1) |
|
256 except ValueError: |
|
257 raise ProcessFormError( |
|
258 form._cw._('badly formated date string %s') % val) |
|
259 |
|
260 |
|
261 class MonthPeriodField(fields.CompoundField): |
|
262 """custom field composed of two subfields, 'begin_month' and 'end_month'. |
|
263 |
|
264 It expects to be used on form that has 'mindate' and 'maxdate' in its |
|
265 extra arguments, telling the range of month to display. |
|
266 """ |
|
267 |
|
268 def __init__(self, *args, **kwargs): |
|
269 kwargs.setdefault('widget', wdgs.IntervalWidget()) |
|
270 super(MonthPeriodField, self).__init__( |
|
271 [fields.StringField(name='begin_month', |
|
272 choices=self.get_range, sort=False, |
|
273 value=self.get_mindate, |
|
274 widget=MonthSelect()), |
|
275 fields.StringField(name='end_month', |
|
276 choices=self.get_range, sort=False, |
|
277 value=self.get_maxdate, |
|
278 widget=MonthSelect())], *args, **kwargs) |
|
279 |
|
280 @staticmethod |
|
281 def get_range(form, field): |
|
282 mindate = date.todate(form.cw_extra_kwargs['mindate']) |
|
283 maxdate = date.todate(form.cw_extra_kwargs['maxdate']) |
|
284 assert mindate <= maxdate |
|
285 _ = form._cw._ |
|
286 months = [] |
|
287 while mindate <= maxdate: |
|
288 label = '%s %s' % (_(calendar.MONTHNAMES[mindate.month - 1]), |
|
289 mindate.year) |
|
290 value = field.widget.format_value(form, field, mindate) |
|
291 months.append( (label, value) ) |
|
292 mindate = date.next_month(mindate) |
|
293 return months |
|
294 |
|
295 @staticmethod |
|
296 def get_mindate(form, field): |
|
297 return form.cw_extra_kwargs['mindate'] |
|
298 |
|
299 @staticmethod |
|
300 def get_maxdate(form, field): |
|
301 return form.cw_extra_kwargs['maxdate'] |
|
302 |
|
303 def process_posted(self, form): |
|
304 for field, value in super(MonthPeriodField, self).process_posted(form): |
|
305 if field.name == 'end_month': |
|
306 value = date.last_day(value) |
|
307 yield field, value |
|
308 |
|
309 |
|
310 Here we first define a widget that will be used to select the beginning and the |
|
311 end of the period, displaying months like '<month> YYYY' but using 'YYYY/mm' as |
|
312 actual value. |
|
313 |
|
314 We then define a field that will actually hold two fields, one for the beginning |
|
315 and another for the end of the period. Each subfield uses the widget we defined |
|
316 earlier, and the outer field itself uses the standard |
|
317 :class:`IntervalWidget`. The field adds some logic: |
|
318 |
|
319 * a vocabulary generation function `get_range`, used to populate each sub-field |
|
320 |
|
321 * two 'value' functions `get_mindate` and `get_maxdate`, used to tell to |
|
322 subfields which value they should consider on form initialization |
|
323 |
|
324 * overriding of `process_posted`, called when the form is being posted, so that |
|
325 the end of the period is properly set to the last day of the month. |
|
326 |
|
327 Now, we can define a very simple form: |
|
328 |
|
329 .. sourcecode:: python |
|
330 |
|
331 class MonthPeriodSelectorForm(forms.FieldsForm): |
|
332 __regid__ = 'myform' |
|
333 __select__ = match_kwargs('mindate', 'maxdate') |
|
334 |
|
335 form_buttons = [wdgs.SubmitButton()] |
|
336 form_renderer_id = 'onerowtable' |
|
337 period = MonthPeriodField() |
|
338 |
|
339 |
|
340 where we simply add our field, set a submit button and use a very simple renderer |
|
341 (try others!). Also we specify a selector that ensures form will have arguments |
|
342 necessary to our field. |
|
343 |
|
344 Now, we need a view that will wrap the form and handle post when it occurs, |
|
345 simply displaying posted values in the page: |
|
346 |
|
347 .. sourcecode:: python |
|
348 |
|
349 class SelfPostingForm(View): |
|
350 __regid__ = 'myformview' |
|
351 |
|
352 def call(self): |
|
353 mindate, maxdate = date.date(2010, 1, 1), date.date(2012, 1, 1) |
|
354 form = self._cw.vreg['forms'].select( |
|
355 'myform', self._cw, mindate=mindate, maxdate=maxdate, action='') |
|
356 try: |
|
357 posted = form.process_posted() |
|
358 self.w(u'<p>posted values %s</p>' % xml_escape(repr(posted))) |
|
359 except RequestError: # no specified period asked |
|
360 pass |
|
361 form.render(w=self.w, formvalues=self._cw.form) |
|
362 |
|
363 |
|
364 Notice usage of the :meth:`process_posted` method, that will return a dictionary |
|
365 of typed values (because they have been processed by the field). In our case, when |
|
366 the form is posted you should see a dictionary with 'begin_month' and 'end_month' |
|
367 as keys with the selected dates as value (as a python `date` object). |
|
368 |
|
369 |
217 APIs |
370 APIs |
218 ~~~~ |
371 ~~~~ |
219 |
372 |
220 .. automodule:: cubicweb.web.formfields |
373 .. automodule:: cubicweb.web.formfields |
221 .. automodule:: cubicweb.web.formwidgets |
374 .. automodule:: cubicweb.web.formwidgets |