|
1 """field classes for form construction |
|
2 |
|
3 :organization: Logilab |
|
4 :copyright: 2009 LOGILAB S.A. (Paris, FRANCE), all rights reserved. |
|
5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr |
|
6 """ |
|
7 __docformat__ = "restructuredtext en" |
|
8 |
|
9 class Field(object): |
|
10 """field class is introduced to control what's displayed in edition form |
|
11 """ |
|
12 widget = TextInput |
|
13 needs_multipart = False |
|
14 creation_rank = 0 |
|
15 |
|
16 def __init__(self, name=None, id=None, label=None, |
|
17 widget=None, required=False, initial=None, |
|
18 choices=None, help=None, eidparam=False): |
|
19 self.required = required |
|
20 if widget is not None: |
|
21 self.widget = widget |
|
22 if isinstance(self.widget, type): |
|
23 self.widget = self.widget() |
|
24 self.name = name |
|
25 self.label = label or name |
|
26 self.id = id or name |
|
27 self.initial = initial |
|
28 self.choices = choices |
|
29 self.help = help |
|
30 self.eidparam = eidparam |
|
31 self.role = 'subject' |
|
32 # global fields ordering in forms |
|
33 self.creation_rank = Field.creation_rank |
|
34 Field.creation_rank += 1 |
|
35 |
|
36 def __unicode__(self): |
|
37 return u'<%s name=%r label=%r id=%r initial=%r>' % ( |
|
38 self.__class__.__name__, self.name, self.label, |
|
39 self.id, self.initial) |
|
40 |
|
41 def __repr__(self): |
|
42 return self.__unicode__().encode('utf-8') |
|
43 |
|
44 def set_name(self, name): |
|
45 assert name |
|
46 self.name = name |
|
47 if not self.id: |
|
48 self.id = name |
|
49 if not self.label: |
|
50 self.label = name |
|
51 |
|
52 def is_visible(self): |
|
53 return not isinstance(self.widget, HiddenInput) |
|
54 |
|
55 def actual_fields(self, form): |
|
56 yield self |
|
57 |
|
58 def format_value(self, req, value): |
|
59 if isinstance(value, (list, tuple)): |
|
60 return [self.format_single_value(req, val) for val in value] |
|
61 return self.format_single_value(req, value) |
|
62 |
|
63 def format_single_value(self, req, value): |
|
64 if value is None: |
|
65 return u'' |
|
66 return unicode(value) |
|
67 |
|
68 def get_widget(self, form): |
|
69 return self.widget |
|
70 |
|
71 def example_format(self, req): |
|
72 return u'' |
|
73 |
|
74 def render(self, form, renderer): |
|
75 return self.get_widget(form).render(form, self) |
|
76 |
|
77 def vocabulary(self, form): |
|
78 if self.choices is not None: |
|
79 return self.choices |
|
80 return form.form_field_vocabulary(self) |
|
81 |
|
82 |
|
83 class StringField(Field): |
|
84 def __init__(self, max_length=None, **kwargs): |
|
85 super(StringField, self).__init__(**kwargs) |
|
86 self.max_length = max_length |
|
87 |
|
88 |
|
89 class TextField(Field): |
|
90 widget = TextArea |
|
91 def __init__(self, rows=10, cols=80, **kwargs): |
|
92 super(TextField, self).__init__(**kwargs) |
|
93 self.rows = rows |
|
94 self.cols = cols |
|
95 |
|
96 |
|
97 class RichTextField(TextField): |
|
98 widget = None |
|
99 def __init__(self, format_field=None, **kwargs): |
|
100 super(RichTextField, self).__init__(**kwargs) |
|
101 self.format_field = format_field |
|
102 |
|
103 def get_widget(self, form): |
|
104 if self.widget is None: |
|
105 if self.use_fckeditor(form): |
|
106 return FCKEditor() |
|
107 return TextArea() |
|
108 return self.widget |
|
109 |
|
110 def get_format_field(self, form): |
|
111 if self.format_field: |
|
112 return self.format_field |
|
113 # we have to cache generated field since it's use as key in the |
|
114 # context dictionnary |
|
115 req = form.req |
|
116 try: |
|
117 return req.data[self] |
|
118 except KeyError: |
|
119 if self.use_fckeditor(form): |
|
120 # if fckeditor is used and format field isn't explicitly |
|
121 # deactivated, we want an hidden field for the format |
|
122 widget = HiddenInput() |
|
123 choices = None |
|
124 else: |
|
125 # else we want a format selector |
|
126 # XXX compute vocabulary |
|
127 widget = Select |
|
128 choices = [(req._(format), format) for format in FormatConstraint().vocabulary(req=req)] |
|
129 field = StringField(name=self.name + '_format', widget=widget, |
|
130 choices=choices) |
|
131 req.data[self] = field |
|
132 return field |
|
133 |
|
134 def actual_fields(self, form): |
|
135 yield self |
|
136 format_field = self.get_format_field(form) |
|
137 if format_field: |
|
138 yield format_field |
|
139 |
|
140 def use_fckeditor(self, form): |
|
141 """return True if fckeditor should be used to edit entity's attribute named |
|
142 `attr`, according to user preferences |
|
143 """ |
|
144 if form.req.use_fckeditor(): |
|
145 return form.form_field_format(self) == 'text/html' |
|
146 return False |
|
147 |
|
148 def render(self, form, renderer): |
|
149 format_field = self.get_format_field(form) |
|
150 if format_field: |
|
151 result = format_field.render(form, renderer) |
|
152 else: |
|
153 result = u'' |
|
154 return result + self.get_widget(form).render(form, self) |
|
155 |
|
156 |
|
157 class FileField(StringField): |
|
158 widget = FileInput |
|
159 needs_multipart = True |
|
160 |
|
161 def __init__(self, format_field=None, encoding_field=None, **kwargs): |
|
162 super(FileField, self).__init__(**kwargs) |
|
163 self.format_field = format_field |
|
164 self.encoding_field = encoding_field |
|
165 |
|
166 def actual_fields(self, form): |
|
167 yield self |
|
168 if self.format_field: |
|
169 yield self.format_field |
|
170 if self.encoding_field: |
|
171 yield self.encoding_field |
|
172 |
|
173 def render(self, form, renderer): |
|
174 wdgs = [self.get_widget(form).render(form, self)] |
|
175 if self.format_field or self.encoding_field: |
|
176 divid = '%s-advanced' % form.context[self]['name'] |
|
177 wdgs.append(u'<a href="%s" title="%s"><img src="%s" alt="%s"/></a>' % |
|
178 (html_escape(toggle_action(divid)), |
|
179 form.req._('show advanced fields'), |
|
180 html_escape(form.req.build_url('data/puce_down.png')), |
|
181 form.req._('show advanced fields'))) |
|
182 wdgs.append(u'<div id="%s" class="hidden">' % divid) |
|
183 if self.format_field: |
|
184 wdgs.append(self.render_subfield(form, self.format_field, renderer)) |
|
185 if self.encoding_field: |
|
186 wdgs.append(self.render_subfield(form, self.encoding_field, renderer)) |
|
187 wdgs.append(u'</div>') |
|
188 if not self.required and form.context[self]['value']: |
|
189 # trick to be able to delete an uploaded file |
|
190 wdgs.append(u'<br/>') |
|
191 wdgs.append(tags.input(name=u'%s__detach' % form.context[self]['name'], |
|
192 type=u'checkbox')) |
|
193 wdgs.append(form.req._('detach attached file')) |
|
194 return u'\n'.join(wdgs) |
|
195 |
|
196 def render_subfield(self, form, field, renderer): |
|
197 return (renderer.render_label(form, field) |
|
198 + field.render(form, renderer) |
|
199 + renderer.render_help(form, field) |
|
200 + u'<br/>') |
|
201 |
|
202 |
|
203 class EditableFileField(FileField): |
|
204 editable_formats = ('text/plain', 'text/html', 'text/rest') |
|
205 |
|
206 def render(self, form, renderer): |
|
207 wdgs = [super(EditableFileField, self).render(form, renderer)] |
|
208 if form.form_field_format(self) in self.editable_formats: |
|
209 data = form.form_field_value(self, {}, load_bytes=True) |
|
210 if data: |
|
211 encoding = form.form_field_encoding(self) |
|
212 try: |
|
213 form.context[self]['value'] = unicode(data.getvalue(), encoding) |
|
214 except UnicodeError: |
|
215 pass |
|
216 else: |
|
217 if not self.required: |
|
218 msg = form.req._( |
|
219 'You can either submit a new file using the browse button above' |
|
220 ', or choose to remove already uploaded file by checking the ' |
|
221 '"detach attached file" check-box, or edit file content online ' |
|
222 'with the widget below.') |
|
223 else: |
|
224 msg = form.req._( |
|
225 'You can either submit a new file using the browse button above' |
|
226 ', or edit file content online with the widget below.') |
|
227 wdgs.append(u'<p><b>%s</b></p>' % msg) |
|
228 wdgs.append(TextArea(setdomid=False).render(form, self)) |
|
229 # XXX restore form context? |
|
230 return '\n'.join(wdgs) |
|
231 |
|
232 |
|
233 class IntField(Field): |
|
234 def __init__(self, min=None, max=None, **kwargs): |
|
235 super(IntField, self).__init__(**kwargs) |
|
236 self.min = min |
|
237 self.max = max |
|
238 |
|
239 |
|
240 class BooleanField(Field): |
|
241 widget = Radio |
|
242 |
|
243 def vocabulary(self, form): |
|
244 if self.choices: |
|
245 return self.choices |
|
246 return [(form.req._('yes'), '1'), (form.req._('no'), '')] |
|
247 |
|
248 |
|
249 class FloatField(IntField): |
|
250 def format_single_value(self, req, value): |
|
251 formatstr = entity.req.property_value('ui.float-format') |
|
252 if value is None: |
|
253 return u'' |
|
254 return formatstr % float(value) |
|
255 |
|
256 def render_example(self, req): |
|
257 return self.format_value(req, 1.234) |
|
258 |
|
259 |
|
260 class DateField(StringField): |
|
261 format_prop = 'ui.date-format' |
|
262 widget = DateTimePicker |
|
263 |
|
264 def format_single_value(self, req, value): |
|
265 return value and ustrftime(value, req.property_value(self.format_prop)) or u'' |
|
266 |
|
267 def render_example(self, req): |
|
268 return self.format_value(req, datetime.now()) |
|
269 |
|
270 |
|
271 class DateTimeField(DateField): |
|
272 format_prop = 'ui.datetime-format' |
|
273 |
|
274 |
|
275 class TimeField(DateField): |
|
276 format_prop = 'ui.datetime-format' |
|
277 |
|
278 |
|
279 class HiddenInitialValueField(Field): |
|
280 def __init__(self, visible_field, name): |
|
281 super(HiddenInitialValueField, self).__init__(name=name, |
|
282 widget=HiddenInput, |
|
283 eidparam=True) |
|
284 self.visible_field = visible_field |
|
285 |
|
286 |
|
287 class RelationField(Field): |
|
288 def __init__(self, **kwargs): |
|
289 super(RelationField, self).__init__(**kwargs) |
|
290 |
|
291 @staticmethod |
|
292 def fromcardinality(card, role, **kwargs): |
|
293 return RelationField(widget=Select(multiple=card in '*+'), |
|
294 **kwargs) |
|
295 |
|
296 def vocabulary(self, form): |
|
297 entity = form.entity |
|
298 req = entity.req |
|
299 # first see if its specified by __linkto form parameters |
|
300 linkedto = entity.linked_to(self.name, self.role) |
|
301 if linkedto: |
|
302 entities = (req.eid_rset(eid).get_entity(0, 0) for eid in linkedto) |
|
303 return [(entity.view('combobox'), entity.eid) for entity in entities] |
|
304 # it isn't, check if the entity provides a method to get correct values |
|
305 res = [] |
|
306 if not self.required: |
|
307 res.append(('', INTERNAL_FIELD_VALUE)) |
|
308 # vocabulary doesn't include current values, add them |
|
309 if entity.has_eid(): |
|
310 rset = entity.related(self.name, self.role) |
|
311 relatedvocab = [(e.view('combobox'), e.eid) for e in rset.entities()] |
|
312 else: |
|
313 relatedvocab = [] |
|
314 return res + form.form_field_vocabulary(self) + relatedvocab |
|
315 |
|
316 def format_single_value(self, req, value): |
|
317 return value |
|
318 |
|
319 |
|
320 def stringfield_from_constraints(constraints, **kwargs): |
|
321 field = None |
|
322 for cstr in constraints: |
|
323 if isinstance(cstr, StaticVocabularyConstraint): |
|
324 return StringField(widget=Select(vocabulary=cstr.vocabulary), |
|
325 **kwargs) |
|
326 if isinstance(cstr, SizeConstraint) and cstr.max is not None: |
|
327 if cstr.max > 257: |
|
328 field = textfield_from_constraint(cstr, **kwargs) |
|
329 else: |
|
330 field = StringField(max_length=cstr.max, **kwargs) |
|
331 return field or TextField(**kwargs) |
|
332 |
|
333 |
|
334 def textfield_from_constraint(constraint, **kwargs): |
|
335 if 256 < constraint.max < 513: |
|
336 rows, cols = 5, 60 |
|
337 else: |
|
338 rows, cols = 10, 80 |
|
339 return TextField(rows, cols, **kwargs) |
|
340 |
|
341 |
|
342 def find_field(eclass, subjschema, rschema, role='subject'): |
|
343 """return the most adapated widget to edit the relation |
|
344 'subjschema rschema objschema' according to information found in the schema |
|
345 """ |
|
346 fieldclass = None |
|
347 if role == 'subject': |
|
348 objschema = rschema.objects(subjschema)[0] |
|
349 cardidx = 0 |
|
350 else: |
|
351 objschema = rschema.subjects(subjschema)[0] |
|
352 cardidx = 1 |
|
353 card = rschema.rproperty(subjschema, objschema, 'cardinality')[cardidx] |
|
354 required = card in '1+' |
|
355 if rschema in eclass.widgets: |
|
356 fieldclass = eclass.widgets[rschema] |
|
357 if isinstance(fieldclass, basestring): |
|
358 return StringField(name=rschema.type) |
|
359 elif not rschema.is_final(): |
|
360 return RelationField.fromcardinality(card, role,name=rschema.type, |
|
361 required=required) |
|
362 else: |
|
363 fieldclass = FIELDS[objschema] |
|
364 if fieldclass is StringField: |
|
365 constraints = rschema.rproperty(subjschema, objschema, 'constraints') |
|
366 return stringfield_from_constraints(constraints, name=rschema.type, |
|
367 required=required) |
|
368 return fieldclass(name=rschema.type, required=required) |
|
369 |
|
370 FIELDS = { |
|
371 'Boolean': BooleanField, |
|
372 'Bytes': FileField, |
|
373 'Date': DateField, |
|
374 'Datetime': DateTimeField, |
|
375 'Int': IntField, |
|
376 'Float': FloatField, |
|
377 'Decimal': StringField, |
|
378 'Password': StringField, |
|
379 'String' : StringField, |
|
380 'Time': TimeField, |
|
381 } |
|
382 views/ |