source: wokkel/data_form.py @ 51:d6bbf24ef053

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

Address Data Form type checking issues.

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

File size: 14.0 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                    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 self.fieldType == 'boolean':
255                value = unicode(value).lower()
256            elif self.fieldType in ('jid-single', 'jid-multi'):
257                value = value.full()
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        if field.fieldType == 'boolean':
299            value = value.lower() in ('1', 'true')
300        elif field.fieldType in ('jid-multi', 'jid-single'):
301            value = JID(value)
302        field.values.append(value)
303
304
305    @staticmethod
306    def fromElement(element):
307        field = Field(None)
308
309        for eAttr, fAttr in {'type': 'fieldType',
310                             'var': 'var',
311                             'label': 'label'}.iteritems():
312            value = element.getAttribute(eAttr)
313            if value:
314                setattr(field, fAttr, value)
315
316
317        for child in element.elements():
318            if child.uri != NS_X_DATA:
319                continue
320
321            func = getattr(Field, '_parse_' + child.name, None)
322            if func:
323                func(field, child)
324
325        return field
326
327
328    @staticmethod
329    def fromDict(dictionary):
330        kwargs = dictionary.copy()
331
332        if 'type' in dictionary:
333            kwargs['fieldType'] = dictionary['type']
334            del kwargs['type']
335
336        if 'options' in dictionary:
337            options = []
338            for value, label in dictionary['options'].iteritems():
339                options.append(Option(value, label))
340            kwargs['options'] = options
341
342        return Field(**kwargs)
343
344
345
346class Form(object):
347    """
348    Data Form.
349
350    There are two similarly named properties of forms. The L{formType} is the
351    the so-called type of the form, and is set as the C{'type'} attribute
352    on the form's root element.
353
354    The Field Standardization specification in XEP-0068, defines a way to
355    provide a context for the field names used in this form, by setting a
356    special hidden field named C{'FORM_TYPE'}, to put the names of all
357    other fields in the namespace of the value of that field. This namespace
358    is recorded in the L{formNamespace} instance variable.
359
360    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
361                    or {'result'}.
362    @type formType: C{str}.
363    @ivar formNamespace: The optional namespace of the field names for this
364                         form. This goes in the special field named
365                         C{'FORM_TYPE'}, if set.
366    @type formNamespace: C{str}.
367    @ivar fields: Dictionary of fields that have a name. Note that this is
368                  meant to be used for reading, only. One should use
369                  L{addField} for adding fields.
370    @type fields: C{dict}
371    """
372
373    def __init__(self, formType, title=None, instructions=None,
374                       formNamespace=None, fields=None):
375        self.formType = formType
376        self.title = title
377        self.instructions = instructions or []
378        self.formNamespace = formNamespace
379
380        self.fieldList = []
381        self.fields = {}
382
383        if fields:
384            for field in fields:
385                self.addField(field)
386
387    def __repr__(self):
388        r = ["Form(formType=", repr(self.formType)]
389
390        if self.title:
391            r.append(", title=")
392            r.append(repr(self.title))
393        if self.instructions:
394            r.append(", instructions=")
395            r.append(repr(self.instructions))
396        if self.formNamespace:
397            r.append(", formNamespace=")
398            r.append(repr(self.formNamespace))
399        if self.fields:
400            r.append(", fields=")
401            r.append(repr(self.fieldList))
402        r.append(")")
403        return u"".join(r)
404
405
406    def addField(self, field):
407        """
408        Add a field to this form.
409
410        Fields are added in order, and L{fields} is a dictionary of the
411        named fields, that is kept in sync only if this method is used for
412        adding new fields. Multiple fields with the same name are disallowed.
413        """
414        if field.var is not None:
415            if field.var in self.fields:
416                raise Error("Duplicate field %r" % field.var)
417
418            self.fields[field.var] = field
419
420        self.fieldList.append(field)
421
422
423    def toElement(self):
424        form = domish.Element((NS_X_DATA, 'x'))
425        form['type'] = self.formType
426
427        if self.title:
428            form.addElement('title', content=self.title)
429
430        for instruction in self.instructions:
431            form.addElement('instruction', content=instruction)
432
433        if self.formNamespace is not None:
434            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
435            form.addChild(field.toElement())
436
437        for field in self.fieldList:
438            form.addChild(field.toElement(self.formType=='form'))
439
440        return form
441
442
443    @staticmethod
444    def _parse_title(form, element):
445        title = unicode(element)
446        if title:
447            form.title = title
448
449
450    @staticmethod
451    def _parse_instructions(form, element):
452        instructions = unicode(element)
453        if instructions:
454            form.instructions.append(instructions)
455
456
457    @staticmethod
458    def _parse_field(form, element):
459        field = Field.fromElement(element)
460        if (field.var == "FORM_TYPE" and
461            field.fieldType == 'hidden' and
462            field.value):
463            form.formNamespace = field.value
464        else:
465            form.addField(field)
466
467    @staticmethod
468    def fromElement(element):
469        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
470            raise Error("Element provided is not a Data Form")
471
472        form = Form(element.getAttribute("type"))
473
474        for child in element.elements():
475            if child.uri != NS_X_DATA:
476                continue
477
478            func = getattr(Form, '_parse_' + child.name, None)
479            if func:
480                func(form, child)
481
482        return form
483
484    def getValues(self):
485        values = {}
486
487        for name, field in self.fields.iteritems():
488            if len(field.values) > 1:
489                value = field.values
490            else:
491                value = field.value
492
493            values[name] = value
494
495        return values
Note: See TracBrowser for help on using the repository browser.