source: wokkel/data_form.py @ 27:d62d7ea14995

Last change on this file since 27:d62d7ea14995 was 25:fd00a744a458, checked in by Ralph Meijer <ralphm@…>, 13 years ago

Refactor Data Forms.

Author: ralphm.
Fixes #13.

This refactoring provides an abstract representation of Forms, Fields and
Options and each of those can be parsed from or unparsed to XML. This change
also simplifies testing in test_pubsub, by allowing the 'received' requests
to be represented as an XML snippit.

File size: 12.3 KB
Line 
1# -*- test-case-name: wokkel.test.test_data_form -*-
2#
3# Copyright (c) 2003-2008 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 toElement(self):
207        """
208        Return the DOM representation of this Field.
209
210        @rtype L{domish.Element}.
211        """
212        if self.var is None and self.fieldType != 'fixed':
213            raise FieldNameRequiredError()
214
215        field = domish.Element((NS_X_DATA, 'field'))
216        field['type'] = self.fieldType
217
218        if self.var is not None:
219            field['var'] = self.var
220
221        if self.values:
222            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
223                                 'text-multi') and
224                len(self.values) > 1):
225                raise TooManyValuesError()
226
227            for value in self.values:
228                if self.fieldType == 'boolean':
229                    # We send out the textual representation of boolean values
230                    value = unicode(bool(value)).lower()
231                elif self.fieldType in ('jid-single', 'jid-multi'):
232                    value = value.full()
233
234                field.addElement('value', content=value)
235
236        if self.fieldType in ('list-single', 'list-multi'):
237            for option in self.options:
238                field.addChild(option.toElement())
239
240        if self.label is not None:
241            field['label'] = self.label
242
243        if self.desc is not None:
244            field.addElement('desc', content=self.desc)
245
246        if self.required:
247            field.addElement('required')
248
249        return field
250
251
252    @staticmethod
253    def _parse_desc(field, element):
254        desc = unicode(element)
255        if desc:
256            field.desc = desc
257
258
259    @staticmethod
260    def _parse_option(field, element):
261        field.options.append(Option.fromElement(element))
262
263
264    @staticmethod
265    def _parse_required(field, element):
266        field.required = True
267
268
269    @staticmethod
270    def _parse_value(field, element):
271        value = unicode(element)
272        if field.fieldType == 'boolean':
273            value = value.lower() in ('1', 'true')
274        elif field.fieldType in ('jid-multi', 'jid-single'):
275            value = JID(value)
276        field.values.append(value)
277
278
279    @staticmethod
280    def fromElement(element):
281        field = Field(None)
282
283        for eAttr, fAttr in {'type': 'fieldType',
284                             'var': 'var',
285                             'label': 'label'}.iteritems():
286            value = element.getAttribute(eAttr)
287            if value:
288                setattr(field, fAttr, value)
289
290
291        for child in element.elements():
292            if child.uri != NS_X_DATA:
293                continue
294
295            func = getattr(Field, '_parse_' + child.name, None)
296            if func:
297                func(field, child)
298
299        return field
300
301
302    @staticmethod
303    def fromDict(dictionary):
304        if 'type' in dictionary:
305            dictionary['fieldType'] = dictionary['type']
306            del dictionary['type']
307        if 'options' in dictionary:
308            options = []
309            for value, label in dictionary['options'].iteritems():
310                options.append(Option(value, label))
311            dictionary['options'] = options
312        return Field(**dictionary)
313
314
315
316class Form(object):
317    """
318    Data Form.
319
320    There are two similarly named properties of forms. The L{formType} is the
321    the so-called type of the form, and is set as the C{'type'} attribute
322    on the form's root element.
323
324    The Field Standardization specification in XEP-0068, defines a way to
325    provide a context for the field names used in this form, by setting a
326    special hidden field named C{'FORM_TYPE'}, to put the names of all
327    other fields in the namespace of the value of that field. This namespace
328    is recorded in the L{formNamespace} instance variable.
329
330    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
331                    or {'result'}.
332    @type formType: C{str}.
333    @ivar formNamespace: The optional namespace of the field names for this
334                         form. This goes in the special field named
335                         C{'FORM_TYPE'}, if set.
336    @type formNamespace: C{str}.
337    """
338
339    def __init__(self, formType, title=None, instructions=None,
340                       formNamespace=None, fields=None):
341        self.formType = formType
342        self.title = title
343        self.instructions = instructions or []
344        self.formNamespace = formNamespace
345        self.fields = fields or []
346
347
348    def __repr__(self):
349        r = ["Form(formType=", repr(self.formType)]
350
351        if self.title:
352            r.append(", title=")
353            r.append(repr(self.title))
354        if self.instructions:
355            r.append(", instructions=")
356            r.append(repr(self.instructions))
357        if self.formNamespace:
358            r.append(", formNamespace=")
359            r.append(repr(self.formNamespace))
360        if self.fields:
361            r.append(", fields=")
362            r.append(repr(self.fields))
363        r.append(")")
364        return u"".join(r)
365
366
367    def toElement(self):
368        form = domish.Element((NS_X_DATA, 'x'))
369        form['type'] = self.formType
370
371        if self.title:
372            form.addElement('title', content=self.title)
373
374        for instruction in self.instructions:
375            form.addElement('instruction', content=instruction)
376
377        if self.formNamespace is not None:
378            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
379            form.addChild(field.toElement())
380
381        for field in self.fields:
382            form.addChild(field.toElement())
383
384        return form
385
386
387    @staticmethod
388    def _parse_title(form, element):
389        title = unicode(element)
390        if title:
391            form.title = title
392
393
394    @staticmethod
395    def _parse_instructions(form, element):
396        instructions = unicode(element)
397        if instructions:
398            form.instructions.append(instructions)
399
400
401    @staticmethod
402    def _parse_field(form, element):
403        field = Field.fromElement(element)
404        if (field.var == "FORM_TYPE" and
405            field.fieldType == 'hidden' and
406            field.value):
407            form.formNamespace = field.value
408        else:
409            form.fields.append(field)
410
411    @staticmethod
412    def fromElement(element):
413        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
414            raise Error("Element provided is not a Data Form")
415
416        form = Form(element.getAttribute("type"))
417
418        for child in element.elements():
419            if child.uri != NS_X_DATA:
420                continue
421
422            func = getattr(Form, '_parse_' + child.name, None)
423            if func:
424                func(form, child)
425
426        return form
427
428    def getValues(self):
429        values = {}
430
431        for field in self.fields:
432            if len(field.values) > 1:
433                value = field.values
434            else:
435                value = field.value
436
437            if field.var:
438                values[field.var] = value
439
440        return values
Note: See TracBrowser for help on using the repository browser.