1 Form construction |
1 HTML form construction |
2 ------------------ |
2 ---------------------- |
3 |
3 |
4 CubicWeb provides usual form/field/widget/renderer abstraction to |
4 CubicWeb provides the somewhat usual form / field / widget / renderer abstraction |
5 provide some generic building blocks which will greatly help you in |
5 to provide generic building blocks which will greatly help you in building forms |
6 building forms properly integrated with CubicWeb (coherent display, |
6 properly integrated with CubicWeb (coherent display, error handling, etc...), |
7 error handling, etc...). |
7 while keeping things as flexible as possible. |
8 |
8 |
9 A form basically only holds a set of fields, and has te be bound to a |
9 A **form** basically only holds a set of **fields**, and has te be bound to a |
10 renderer which is responsible to layout them. Each field is bound to a |
10 **renderer** which is responsible to layout them. Each field is bound to a |
11 widget that will be used to fill in value(s) for that field (at form |
11 **widget** that will be used to fill in value(s) for that field (at form |
12 generation time) and 'decode' (fetch and give a proper Python type to) |
12 generation time) and 'decode' (fetch and give a proper Python type to) values |
13 values sent back by the browser. |
13 sent back by the browser. |
14 |
14 |
15 The Field class and basic fields |
15 The **field** should be used according to the type of what you want to edit. |
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
16 E.g. if you want to edit some date, you'll have to use the |
17 |
17 :class:`~cubicweb.web.formfields.DateField`. Then you can choose among multiple |
18 .. autoclass:: cubicweb.web.formfields.Field |
18 widgets to edit it, for instance :class:`~cubicweb.web.formwidgets.TextInput` (a |
19 |
19 bare text field), :class:`~cubicweb.web.formwidgets.DateTimePicker` (a simple |
20 Existing field types are: |
20 calendar) or even :class:`~cubicweb.web.formwidgets.JQueryDatePicker` (the JQuery |
21 |
21 calendar). You can of course also write your own widget. |
22 .. autoclass:: cubicweb.web.formfields.StringField |
22 |
23 .. autoclass:: cubicweb.web.formfields.PasswordField |
23 |
24 .. autoclass:: cubicweb.web.formfields.RichTextField |
24 .. automodule:: cubicweb.web.formfields |
25 .. autoclass:: cubicweb.web.formfields.FileField |
25 .. automodule:: cubicweb.web.formwidgets |
26 .. autoclass:: cubicweb.web.formfields.EditableFileField |
26 .. automodule:: cubicweb.web.views.forms |
27 .. autoclass:: cubicweb.web.formfields.IntField |
27 .. automodule:: cubicweb.web.views.autoform |
28 .. autoclass:: cubicweb.web.formfields.BooleanField |
28 .. automodule:: cubicweb.web.views.formrenderers |
29 .. autoclass:: cubicweb.web.formfields.FloatField |
29 |
30 .. autoclass:: cubicweb.web.formfields.DateField |
30 |
31 .. autoclass:: cubicweb.web.formfields.DateTimeField |
31 Now what ? Example of bare fields form |
32 .. autoclass:: cubicweb.web.formfields.TimeField |
32 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
33 .. autoclass:: cubicweb.web.formfields.RelationField |
33 |
34 .. autoclass:: cubicweb.web.formfields.CompoundField |
34 We want to define a form doing something else than editing an entity. The idea is |
35 |
35 to propose a form to send an email to entities in a resultset which implements |
36 |
36 :class:`IEmailable`. Let's take a simplified version of what you'll find in |
37 Widgets |
37 :mod:`cubicweb.web.views.massmailing`. |
38 ~~~~~~~ |
38 |
39 Base class for widget is :class:cubicweb.web.formwidgets.FieldWidget class. |
39 Here is the source code: |
40 |
40 |
41 Existing widget types are: |
41 .. sourcecode:: python |
42 |
42 |
43 .. autoclass:: cubicweb.web.formwidgets.HiddenInput |
43 def sender_value(form): |
44 .. autoclass:: cubicweb.web.formwidgets.TextInput |
44 return '%s <%s>' % (form._cw.user.dc_title(), form._cw.user.get_email()) |
45 .. autoclass:: cubicweb.web.formwidgets.PasswordInput |
45 |
46 .. autoclass:: cubicweb.web.formwidgets.PasswordSingleInput |
46 def recipient_choices(form, field): |
47 .. autoclass:: cubicweb.web.formwidgets.FileInput |
47 return [(e.get_email(), e.eid) for e in form.cw_rset.entities() |
48 .. autoclass:: cubicweb.web.formwidgets.ButtonInput |
48 if e.get_email()] |
49 .. autoclass:: cubicweb.web.formwidgets.TextArea |
49 |
50 .. autoclass:: cubicweb.web.formwidgets.FCKEditor |
50 def recipient_value(form): |
51 .. autoclass:: cubicweb.web.formwidgets.Select |
51 return [e.eid for e in form.cw_rset.entities() if e.get_email()] |
52 .. autoclass:: cubicweb.web.formwidgets.CheckBox |
52 |
53 .. autoclass:: cubicweb.web.formwidgets.Radio |
53 class MassMailingForm(forms.FieldsForm): |
54 .. autoclass:: cubicweb.web.formwidgets.DateTimePicker |
54 __regid__ = 'massmailing' |
55 .. autoclass:: cubicweb.web.formwidgets.JQueryDateTimePicker |
55 |
56 .. autoclass:: cubicweb.web.formwidgets.JQueryDatePicker |
56 needs_js = ('cubicweb.widgets.js',) |
57 .. autoclass:: cubicweb.web.formwidgets.JQueryTimePicker |
57 domid = 'sendmail' |
58 .. autoclass:: cubicweb.web.formwidgets.AjaxWidget |
58 action = 'sendmail' |
59 .. autoclass:: cubicweb.web.formwidgets.AutoCompletionWidget |
59 |
60 .. autoclass:: cubicweb.web.formwidgets.EditableURLWidget |
60 sender = ff.StringField(widget=TextInput({'disabled': 'disabled'}), |
61 |
61 label=_('From:'), |
62 Other classes in this module, which are not proper widget (they are not associated to |
62 value=sender_value) |
63 field) but are used as form controls, may also be useful: Button, SubmitButton, |
63 |
64 ResetButton, ImgButton, |
64 recipient = ff.StringField(widget=CheckBox(), |
65 |
65 label=_('Recipients:'), |
66 |
66 choices=recipient_choices, |
67 Of course you can not use any widget with any field... |
67 value=recipients_value) |
68 |
68 |
69 Renderers |
69 subject = ff.StringField(label=_('Subject:'), max_length=256) |
70 ~~~~~~~~~ |
70 |
71 |
71 mailbody = ff.StringField(widget=AjaxWidget(wdgtype='TemplateTextField', |
72 .. autoclass:: cubicweb.web.views.formrenderers.BaseFormRenderer |
72 inputid='mailbody')) |
73 .. autoclass:: cubicweb.web.views.formrenderers.HTableFormRenderer |
73 |
74 .. autoclass:: cubicweb.web.views.formrenderers.EntityCompositeFormRenderer |
74 form_buttons = [ImgButton('sendbutton', "javascript: $('#sendmail').submit()", |
75 .. autoclass:: cubicweb.web.views.formrenderers.EntityFormRenderer |
75 _('send email'), 'SEND_EMAIL_ICON'), |
76 .. autoclass:: cubicweb.web.views.formrenderers.EntityInlinedFormRenderer |
76 ImgButton('cancelbutton', "javascript: history.back()", |
77 |
77 stdmsgs.BUTTON_CANCEL, 'CANCEL_EMAIL_ICON')] |
|
78 |
|
79 Let's detail what's going on up there. Our form will hold four fields: |
|
80 |
|
81 * a sender field, which is disabled and will simply contains the user's name and |
|
82 email |
|
83 |
|
84 * a recipients field, which will be displayed as a list of users in the context |
|
85 result set with checkboxes so user can still choose who will receive his mailing |
|
86 by checking or not the checkboxes. By default all of them will be checked since |
|
87 field's value return a list containing same eids as those returned by the |
|
88 vocabulary function. |
|
89 |
|
90 * a subject field, limited to 256 characters (hence we know a |
|
91 :class:`~cubicweb.web.formwidgets.TextInput` will be used, as explained in |
|
92 :class:`~cubicweb.web.formfields.StringField`) |
|
93 |
|
94 * a mailbody field. This field use an ajax widget, defined in `cubicweb.widgets.js`, |
|
95 and whose definition won't be shown here. Notice though that we tell this form |
|
96 need this javascript file by using `needs_js` |
|
97 |
|
98 Last but not least, we add two buttons control: one to post the form using |
|
99 javascript (`$('#sendmail')` being the jQuery call to get the element with DOM id |
|
100 set to 'sendmail', which is our form DOM id as specified by its `domid` |
|
101 attribute), another to cancel the form which will go back to the previous page |
|
102 using another javascript call. Also we specify image to used as button icon a |
|
103 resource identifier (see :ref:`external_resources`) given as last argument to |
|
104 :class:`cubicweb.web.formwidgets.ImgButton`. |
|
105 |
|
106 To see this form, we still have to wrap it in a view. This is pretty simple: |
|
107 |
|
108 .. sourcecode:: python |
|
109 |
|
110 class MassMailingFormView(form.FormViewMixIn, EntityView): |
|
111 __regid__ = 'massmailing' |
|
112 __select__ = implements(IEmailable) & authenticated_user() |
|
113 |
|
114 def call(self): |
|
115 form = self._cw.vreg['forms'].select('massmailing', self._cw, |
|
116 rset=self.cw_rset) |
|
117 self.w(form.render()) |
|
118 |
|
119 As you see, we simply define a view with proper selector so it only apply to a |
|
120 result set containing :class:`IEmailable` entities, and so that only users in the |
|
121 managers or users group can use it. Then in the `call()` method for this view we |
|
122 simply select the above form and write what its `.render()` method returns. |
|
123 |
|
124 When this form is submitted, a controller with id 'sendmail' will be called (as |
|
125 specified using `action`). This controller will be responsible to actually send |
|
126 the mail to specified recipients. |
|
127 |
|
128 Here is what it looks like: |
|
129 |
|
130 .. sourcecode:: python |
|
131 |
|
132 class SendMailController(Controller): |
|
133 __regid__ = 'sendmail' |
|
134 __select__ = authenticated_user() & match_form_params('recipient', 'mailbody', 'subject') |
|
135 |
|
136 def publish(self, rset=None): |
|
137 body = self._cw.form['mailbody'] |
|
138 subject = self._cw.form['subject'] |
|
139 eids = self._cw.form['recipient'] |
|
140 # eids may be a string if only one recipient was specified |
|
141 if isinstance(eids, basestring): |
|
142 rset = self._cw.execute('Any X WHERE X eid %(x)s', {'x': eids}) |
|
143 else: |
|
144 rset = self._cw.execute('Any X WHERE X eid in (%s)' % (','.join(eids))) |
|
145 recipients = list(rset.entities()) |
|
146 msg = format_mail({'email' : self._cw.user.get_email(), |
|
147 'name' : self._cw.user.dc_title()}, |
|
148 recipients, body, subject) |
|
149 if not self._cw.vreg.config.sendmails([(msg, recipients]): |
|
150 msg = self._cw._('could not connect to the SMTP server') |
|
151 else: |
|
152 msg = self._cw._('emails successfully sent') |
|
153 raise Redirect(self._cw.build_url(__message=msg)) |
|
154 |
|
155 |
|
156 The entry point of a controller is the publish method. In that case we simply get |
|
157 back post values in request's `form` attribute, get user instances according |
|
158 to eids found in the 'recipient' form value, and send email after calling |
|
159 :func:`format_mail` to get a proper email message. If we can't send email or |
|
160 if we successfully sent email, we redirect to the index page with proper message |
|
161 to inform the user. |
|
162 |
|
163 Also notice that our controller has a selector that deny access to it to |
|
164 anonymous users (we don't want our instance to be used as a spam relay), but also |
|
165 check expected parameters are specified in forms. That avoid later defensive |
|
166 programming (though it's not enough to handle all possible error cases). |
|
167 |
|
168 To conclude our example, suppose we wish a different form layout and that existent |
|
169 renderers are not satisfying (we would check that first of course :). We would then |
|
170 have to define our own renderer: |
|
171 |
|
172 .. sourcecode:: python |
|
173 |
|
174 class MassMailingFormRenderer(formrenderers.FormRenderer): |
|
175 __regid__ = 'massmailing' |
|
176 |
|
177 def _render_fields(self, fields, w, form): |
|
178 w(u'<table class="headersform">') |
|
179 for field in fields: |
|
180 if field.name == 'mailbody': |
|
181 w(u'</table>') |
|
182 w(u'<div id="toolbar">') |
|
183 w(u'<ul>') |
|
184 for button in form.form_buttons: |
|
185 w(u'<li>%s</li>' % button.render(form)) |
|
186 w(u'</ul>') |
|
187 w(u'</div>') |
|
188 w(u'<div>') |
|
189 w(field.render(form, self)) |
|
190 w(u'</div>') |
|
191 else: |
|
192 w(u'<tr>') |
|
193 w(u'<td class="hlabel">%s</td>' % self.render_label(form, field)) |
|
194 w(u'<td class="hvalue">') |
|
195 w(field.render(form, self)) |
|
196 w(u'</td></tr>') |
|
197 |
|
198 def render_buttons(self, w, form): |
|
199 pass |
|
200 |
|
201 We simply override the `_render_fields` and `render_buttons` method of the base form renderer |
|
202 to arrange fields as we desire it: here we'll have first a two columns table with label and |
|
203 value of the sender, recipients and subject field (form order respected), then form controls, |
|
204 then a div containing the textarea for the email's content. |
|
205 |
|
206 To bind this renderer to our form, we should add to our form definition above: |
|
207 |
|
208 .. sourcecode:: python |
|
209 |
|
210 form_renderer_id = 'massmailing' |
|
211 |
|
212 |
|
213 .. Example of entity fields form |