source: wokkel/data_form.py @ 50:a37c65be8203

Last change on this file since 50:a37c65be8203 was 50:a37c65be8203, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Selectively render Data Forms fields depending on form type.

Author: ralphm.
Reviewer: tofu.
Fixes #40.

File size: 13.6 KB
Line 
1# -*- test-case-name: wokkel.test.test_data_form -*-
2#
3# Copyright (c) 2003-2009 Ralph Meijer
4# See LICENSE for details.
5
6"""
7Data Forms.
8
9Support for Data Forms as described in
10U{XEP-0004<http://www.xmpp.org/extensions/xep-0004.html>}, along with support
11for Field Standardization for Data Forms as described in
12U{XEP-0068<http://www.xmpp.org/extensions/xep-0068.html>}.
13"""
14
15from twisted.words.protocols.jabber.jid import JID
16from twisted.words.xish import domish
17
18NS_X_DATA = 'jabber:x:data'
19
20
21
22class Error(Exception):
23    """
24    Data Forms error.
25    """
26
27
28
29class FieldNameRequiredError(Error):
30    """
31    A field name is required for this field type.
32    """
33
34
35
36class TooManyValuesError(Error):
37    """
38    This field is single-value.
39    """
40
41
42
43class Option(object):
44    """
45    Data Forms field option.
46
47    @ivar value: Value of this option.
48    @type value: C{unicode}
49    @ivar label: Optional label for this option.
50    @type label: C{unicode} or C{NoneType}.
51    """
52
53    def __init__(self, value, label=None):
54        self.value = value
55        self.label = label
56
57
58    def __repr__(self):
59        r = ["Option(", repr(self.value)]
60        if self.label:
61            r.append(", ")
62            r.append(repr(self.label))
63        r.append(")")
64        return u"".join(r)
65
66
67    def toElement(self):
68        """
69        Return the DOM representation of this option.
70
71        @rtype: L{domish.Element}.
72        """
73        option = domish.Element((NS_X_DATA, 'option'))
74        option.addElement('value', content=self.value)
75        if self.label:
76            option['label'] = self.label
77        return option
78
79    @staticmethod
80    def fromElement(element):
81        valueElements = list(domish.generateElementsQNamed(element.children,
82                                                           'value', NS_X_DATA))
83        if not valueElements:
84            raise Error("Option has no value")
85
86        label = element.getAttribute('label')
87        return Option(unicode(valueElements[0]), label)
88
89
90class Field(object):
91    """
92    Data Forms field.
93
94    @ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'},
95                     C{'hidden'}, C{'jid-multi'}, C{'jid-single'},
96                     C{'list-multi'}, {'list-single'}, C{'text-multi'},
97                     C{'text-private'}, C{'text-single'}.
98
99                     The default is C{'text-single'}.
100    @type fieldType: C{str}
101    @ivar var: Field name. Optional if L{fieldType} is C{'fixed'}.
102    @type var: C{str}
103    @ivar label: Human readable label for this field.
104    @type label: C{unicode}
105    @ivar values: The values for this field, for multi-valued field
106                  types, as a list of C{bool}, C{unicode} or L{JID}.
107    @type values: C{list}
108    @ivar options: List of possible values to choose from in a response
109                   to this form as a list of L{Option}s.
110    @type options: C{list}.
111    @ivar desc: Human readable description for this field.
112    @type desc: C{unicode}
113    @ivar required: Whether the field is required to be provided in a
114                    response to this form.
115    @type required: C{bool}.
116    """
117
118    def __init__(self, fieldType='text-single', var=None, value=None,
119                       values=None, options=None, label=None, desc=None,
120                       required=False):
121        """
122        Initialize this field.
123
124        See the identically named instance variables for descriptions.
125
126        If C{value} is not C{None}, it overrides C{values}, setting the
127        given value as the only value for this field.
128        """
129
130        self.fieldType = fieldType
131        self.var = var
132        if value is not None:
133            self.value = value
134        else:
135            self.values = values or []
136
137        try:
138            self.options = [Option(value, label)
139                            for value, label in options.iteritems()]
140        except AttributeError:
141            self.options = options or []
142
143        self.label = label
144        self.desc = desc
145        self.required = required
146
147
148    def __repr__(self):
149        r = ["Field(fieldType=", repr(self.fieldType)]
150        if self.var:
151            r.append(", var=")
152            r.append(repr(self.var))
153        if self.label:
154            r.append(", label=")
155            r.append(repr(self.label))
156        if self.desc:
157            r.append(", desc=")
158            r.append(repr(self.desc))
159        if self.required:
160            r.append(", required=")
161            r.append(repr(self.required))
162        if self.values:
163            r.append(", values=")
164            r.append(repr(self.values))
165        if self.options:
166            r.append(", options=")
167            r.append(repr(self.options))
168        r.append(")")
169        return u"".join(r)
170
171
172    def __value_set(self, value):
173        """
174        Setter of value property.
175
176        Sets C{value} as the only element of L{values}.
177
178        @type value: C{bool}, C{unicode} or L{JID}
179        """
180        self.values = [value]
181
182
183    def __value_get(self):
184        """
185        Getter of value property.
186
187        Returns the first element of L{values}, if present, or C{None}.
188        """
189
190        if self.values:
191            return self.values[0]
192        else:
193            return None
194
195
196    value = property(__value_get, __value_set, doc="""
197            The value for this field, for single-valued field types.
198
199            This is a special property accessing L{values}.  Writing to this
200            property empties L{values} and then sets the given value as the
201            only element of L{values}.  Reading from this propery returns the
202            first element of L{values}.
203            """)
204
205
206    def typeCheck(self):
207        """
208        Check field properties agains the set field type.
209        """
210        if self.var is None and self.fieldType != 'fixed':
211            raise FieldNameRequiredError()
212
213        if self.values:
214            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
215                                 'text-multi') and
216                len(self.values) > 1):
217                raise TooManyValuesError()
218
219            newValues = []
220            for value in self.values:
221                if self.fieldType == 'boolean':
222                    # We send out the textual representation of boolean values
223                    value = bool(int(value))
224                elif self.fieldType in ('jid-single', 'jid-multi'):
225                    value = value.full()
226
227                newValues.append(value)
228
229            self.values = newValues
230
231    def toElement(self, asForm=False):
232        """
233        Return the DOM representation of this Field.
234
235        @rtype: L{domish.Element}.
236        """
237
238        self.typeCheck()
239
240        field = domish.Element((NS_X_DATA, 'field'))
241
242        if asForm or self.fieldType != 'text-single':
243            field['type'] = self.fieldType
244
245        if self.var is not None:
246            field['var'] = self.var
247
248        for value in self.values:
249            if self.fieldType == 'boolean':
250                value = unicode(value).lower()
251            field.addElement('value', content=value)
252
253        if asForm:
254            if self.fieldType in ('list-single', 'list-multi'):
255                for option in self.options:
256                    field.addChild(option.toElement())
257
258            if self.label is not None:
259                field['label'] = self.label
260
261            if self.desc is not None:
262                field.addElement('desc', content=self.desc)
263
264            if self.required:
265                field.addElement('required')
266
267        return field
268
269
270    @staticmethod
271    def _parse_desc(field, element):
272        desc = unicode(element)
273        if desc:
274            field.desc = desc
275
276
277    @staticmethod
278    def _parse_option(field, element):
279        field.options.append(Option.fromElement(element))
280
281
282    @staticmethod
283    def _parse_required(field, element):
284        field.required = True
285
286
287    @staticmethod
288    def _parse_value(field, element):
289        value = unicode(element)
290        if field.fieldType == 'boolean':
291            value = value.lower() in ('1', 'true')
292        elif field.fieldType in ('jid-multi', 'jid-single'):
293            value = JID(value)
294        field.values.append(value)
295
296
297    @staticmethod
298    def fromElement(element):
299        field = Field(None)
300
301        for eAttr, fAttr in {'type': 'fieldType',
302                             'var': 'var',
303                             'label': 'label'}.iteritems():
304            value = element.getAttribute(eAttr)
305            if value:
306                setattr(field, fAttr, value)
307
308
309        for child in element.elements():
310            if child.uri != NS_X_DATA:
311                continue
312
313            func = getattr(Field, '_parse_' + child.name, None)
314            if func:
315                func(field, child)
316
317        return field
318
319
320    @staticmethod
321    def fromDict(dictionary):
322        kwargs = dictionary.copy()
323
324        if 'type' in dictionary:
325            kwargs['fieldType'] = dictionary['type']
326            del kwargs['type']
327
328        if 'options' in dictionary:
329            options = []
330            for value, label in dictionary['options'].iteritems():
331                options.append(Option(value, label))
332            kwargs['options'] = options
333
334        return Field(**kwargs)
335
336
337
338class Form(object):
339    """
340    Data Form.
341
342    There are two similarly named properties of forms. The L{formType} is the
343    the so-called type of the form, and is set as the C{'type'} attribute
344    on the form's root element.
345
346    The Field Standardization specification in XEP-0068, defines a way to
347    provide a context for the field names used in this form, by setting a
348    special hidden field named C{'FORM_TYPE'}, to put the names of all
349    other fields in the namespace of the value of that field. This namespace
350    is recorded in the L{formNamespace} instance variable.
351
352    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
353                    or {'result'}.
354    @type formType: C{str}.
355    @ivar formNamespace: The optional namespace of the field names for this
356                         form. This goes in the special field named
357                         C{'FORM_TYPE'}, if set.
358    @type formNamespace: C{str}.
359    @ivar fields: Dictionary of fields that have a name. Note that this is
360                  meant to be used for reading, only. One should use
361                  L{addField} for adding fields.
362    @type fields: C{dict}
363    """
364
365    def __init__(self, formType, title=None, instructions=None,
366                       formNamespace=None, fields=None):
367        self.formType = formType
368        self.title = title
369        self.instructions = instructions or []
370        self.formNamespace = formNamespace
371
372        self.fieldList = []
373        self.fields = {}
374
375        if fields:
376            for field in fields:
377                self.addField(field)
378
379    def __repr__(self):
380        r = ["Form(formType=", repr(self.formType)]
381
382        if self.title:
383            r.append(", title=")
384            r.append(repr(self.title))
385        if self.instructions:
386            r.append(", instructions=")
387            r.append(repr(self.instructions))
388        if self.formNamespace:
389            r.append(", formNamespace=")
390            r.append(repr(self.formNamespace))
391        if self.fields:
392            r.append(", fields=")
393            r.append(repr(self.fieldList))
394        r.append(")")
395        return u"".join(r)
396
397
398    def addField(self, field):
399        """
400        Add a field to this form.
401
402        Fields are added in order, and L{fields} is a dictionary of the
403        named fields, that is kept in sync only if this method is used for
404        adding new fields. Multiple fields with the same name are disallowed.
405        """
406        if field.var is not None:
407            if field.var in self.fields:
408                raise Error("Duplicate field %r" % field.var)
409
410            self.fields[field.var] = field
411
412        self.fieldList.append(field)
413
414
415    def toElement(self):
416        form = domish.Element((NS_X_DATA, 'x'))
417        form['type'] = self.formType
418
419        if self.title:
420            form.addElement('title', content=self.title)
421
422        for instruction in self.instructions:
423            form.addElement('instruction', content=instruction)
424
425        if self.formNamespace is not None:
426            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
427            form.addChild(field.toElement())
428
429        for field in self.fieldList:
430            form.addChild(field.toElement(self.formType=='form'))
431
432        return form
433
434
435    @staticmethod
436    def _parse_title(form, element):
437        title = unicode(element)
438        if title:
439            form.title = title
440
441
442    @staticmethod
443    def _parse_instructions(form, element):
444        instructions = unicode(element)
445        if instructions:
446            form.instructions.append(instructions)
447
448
449    @staticmethod
450    def _parse_field(form, element):
451        field = Field.fromElement(element)
452        if (field.var == "FORM_TYPE" and
453            field.fieldType == 'hidden' and
454            field.value):
455            form.formNamespace = field.value
456        else:
457            form.addField(field)
458
459    @staticmethod
460    def fromElement(element):
461        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
462            raise Error("Element provided is not a Data Form")
463
464        form = Form(element.getAttribute("type"))
465
466        for child in element.elements():
467            if child.uri != NS_X_DATA:
468                continue
469
470            func = getattr(Form, '_parse_' + child.name, None)
471            if func:
472                func(form, child)
473
474        return form
475
476    def getValues(self):
477        values = {}
478
479        for name, field in self.fields.iteritems():
480            if len(field.values) > 1:
481                value = field.values
482            else:
483                value = field.value
484
485            values[name] = value
486
487        return values
Note: See TracBrowser for help on using the repository browser.