source: wokkel/data_form.py @ 78:c85ce41a75bc

Last change on this file since 78:c85ce41a75bc was 78:c85ce41a75bc, checked in by Ralph Meijer <ralphm@…>, 12 years ago

Add missing tests for Data Forms and correct some minor issues.

File size: 14.7 KB
Line 
1# -*- test-case-name: wokkel.test.test_data_form -*-
2#
3# Copyright (c) 2003-2010 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                    if isinstance(value, (str, unicode)):
223                        checkValue = value.lower()
224                        if not checkValue in ('0', '1', 'false', 'true'):
225                            raise ValueError("Not a boolean")
226                        value = checkValue in ('1', 'true')
227                    value = bool(value)
228                elif self.fieldType in ('jid-single', 'jid-multi'):
229                    if not hasattr(value, 'full'):
230                        value = JID(value)
231
232                newValues.append(value)
233
234            self.values = newValues
235
236    def toElement(self, asForm=False):
237        """
238        Return the DOM representation of this Field.
239
240        @rtype: L{domish.Element}.
241        """
242
243        self.typeCheck()
244
245        field = domish.Element((NS_X_DATA, 'field'))
246
247        if asForm or self.fieldType != 'text-single':
248            field['type'] = self.fieldType
249
250        if self.var is not None:
251            field['var'] = self.var
252
253        for value in self.values:
254            if isinstance(value, bool):
255                value = unicode(value).lower()
256            else:
257                value = unicode(value)
258
259            field.addElement('value', content=value)
260
261        if asForm:
262            if self.fieldType in ('list-single', 'list-multi'):
263                for option in self.options:
264                    field.addChild(option.toElement())
265
266            if self.label is not None:
267                field['label'] = self.label
268
269            if self.desc is not None:
270                field.addElement('desc', content=self.desc)
271
272            if self.required:
273                field.addElement('required')
274
275        return field
276
277
278    @staticmethod
279    def _parse_desc(field, element):
280        desc = unicode(element)
281        if desc:
282            field.desc = desc
283
284
285    @staticmethod
286    def _parse_option(field, element):
287        field.options.append(Option.fromElement(element))
288
289
290    @staticmethod
291    def _parse_required(field, element):
292        field.required = True
293
294
295    @staticmethod
296    def _parse_value(field, element):
297        value = unicode(element)
298        field.values.append(value)
299
300
301    @staticmethod
302    def fromElement(element):
303        field = Field(None)
304
305        for eAttr, fAttr in {'type': 'fieldType',
306                             'var': 'var',
307                             'label': 'label'}.iteritems():
308            value = element.getAttribute(eAttr)
309            if value:
310                setattr(field, fAttr, value)
311
312
313        for child in element.elements():
314            if child.uri != NS_X_DATA:
315                continue
316
317            func = getattr(Field, '_parse_' + child.name, None)
318            if func:
319                func(field, child)
320
321        return field
322
323
324    @staticmethod
325    def fromDict(dictionary):
326        kwargs = dictionary.copy()
327
328        if 'type' in dictionary:
329            kwargs['fieldType'] = dictionary['type']
330            del kwargs['type']
331
332        if 'options' in dictionary:
333            options = []
334            for value, label in dictionary['options'].iteritems():
335                options.append(Option(value, label))
336            kwargs['options'] = options
337
338        return Field(**kwargs)
339
340
341
342class Form(object):
343    """
344    Data Form.
345
346    There are two similarly named properties of forms. The C{formType} is the
347    the so-called type of the form, and is set as the C{'type'} attribute
348    on the form's root element.
349
350    The Field Standardization specification in XEP-0068, defines a way to
351    provide a context for the field names used in this form, by setting a
352    special hidden field named C{'FORM_TYPE'}, to put the names of all
353    other fields in the namespace of the value of that field. This namespace
354    is recorded in the C{formNamespace} instance variable.
355
356    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
357                    or {'result'}.
358    @type formType: C{str}
359
360    @ivar title: Natural language title of the form.
361    @type title: C{unicode}
362
363    @ivar instructions: Natural language instructions as a list of C{unicode}
364        strings without line breaks.
365    @type instructions: C{list}
366
367    @ivar formNamespace: The optional namespace of the field names for this
368        form. This goes in the special field named C{'FORM_TYPE'}, if set.
369    @type formNamespace: C{str}.
370
371    @ivar fields: Dictionary of named fields. Note that this is meant to be
372        used for reading, only. One should use L{addField} or L{makeFields} and
373        L{removeField} for adding and removing fields.
374    @type fields: C{dict}
375
376    @ivar fieldList: List of all fields, in the order they are added. Like
377        C{fields}, this is meant to be used for reading, only.
378    @type fieldList: C{list}
379    """
380
381    def __init__(self, formType, title=None, instructions=None,
382                       formNamespace=None, fields=None):
383        self.formType = formType
384        self.title = title
385        self.instructions = instructions or []
386        self.formNamespace = formNamespace
387
388        self.fieldList = []
389        self.fields = {}
390
391        if fields:
392            for field in fields:
393                self.addField(field)
394
395    def __repr__(self):
396        r = ["Form(formType=", repr(self.formType)]
397
398        if self.title:
399            r.append(", title=")
400            r.append(repr(self.title))
401        if self.instructions:
402            r.append(", instructions=")
403            r.append(repr(self.instructions))
404        if self.formNamespace:
405            r.append(", formNamespace=")
406            r.append(repr(self.formNamespace))
407        if self.fieldList:
408            r.append(", fields=")
409            r.append(repr(self.fieldList))
410        r.append(")")
411        return u"".join(r)
412
413
414    def addField(self, field):
415        """
416        Add a field to this form.
417
418        Fields are added in order, and L{fields} is a dictionary of the
419        named fields, that is kept in sync only if this method is used for
420        adding new fields. Multiple fields with the same name are disallowed.
421        """
422        if field.var is not None:
423            if field.var in self.fields:
424                raise Error("Duplicate field %r" % field.var)
425
426            self.fields[field.var] = field
427
428        self.fieldList.append(field)
429
430
431    def toElement(self):
432        form = domish.Element((NS_X_DATA, 'x'))
433        form['type'] = self.formType
434
435        if self.title:
436            form.addElement('title', content=self.title)
437
438        for instruction in self.instructions:
439            form.addElement('instructions', content=instruction)
440
441        if self.formNamespace is not None:
442            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
443            form.addChild(field.toElement())
444
445        for field in self.fieldList:
446            form.addChild(field.toElement(self.formType=='form'))
447
448        return form
449
450
451    @staticmethod
452    def _parse_title(form, element):
453        title = unicode(element)
454        if title:
455            form.title = title
456
457
458    @staticmethod
459    def _parse_instructions(form, element):
460        instructions = unicode(element)
461        if instructions:
462            form.instructions.append(instructions)
463
464
465    @staticmethod
466    def _parse_field(form, element):
467        field = Field.fromElement(element)
468        if (field.var == "FORM_TYPE" and
469            field.fieldType == 'hidden' and
470            field.value):
471            form.formNamespace = field.value
472        else:
473            form.addField(field)
474
475    @staticmethod
476    def fromElement(element):
477        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
478            raise Error("Element provided is not a Data Form")
479
480        form = Form(element.getAttribute("type"))
481
482        for child in element.elements():
483            if child.uri != NS_X_DATA:
484                continue
485
486            func = getattr(Form, '_parse_' + child.name, None)
487            if func:
488                func(form, child)
489
490        return form
491
492
493    def getValues(self):
494        """
495        Extract values from the named form fields.
496
497        For all named fields, the corresponding value or values are
498        returned in a dictionary keyed by the field name. For multi-value
499        fields, the dictionary value is a list, otherwise a single value.
500
501        If a field has no type, and the field has multiple values, the value of
502        the dictionary entry is the list of values. Otherwise, it will be a
503        single value.
504
505        @rtype: C{dict}
506        """
507        values = {}
508
509        for name, field in self.fields.iteritems():
510            if (field.fieldType in ('jid-multi', 'list-multi', 'text-multi') or
511                (field.fieldType is None and len(field.values) > 1)):
512                value = field.values
513            else:
514                value = field.value
515
516            values[name] = value
517
518        return values
Note: See TracBrowser for help on using the repository browser.