source: wokkel/data_form.py @ 56:01740c205934

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

Don't do type interpretation on parsing Data Forms fields.

File size: 13.8 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        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 L{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 L{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    @ivar formNamespace: The optional namespace of the field names for this
360                         form. This goes in the special field named
361                         C{'FORM_TYPE'}, if set.
362    @type formNamespace: C{str}.
363    @ivar fields: Dictionary of fields that have a name. Note that this is
364                  meant to be used for reading, only. One should use
365                  L{addField} for adding fields.
366    @type fields: C{dict}
367    """
368
369    def __init__(self, formType, title=None, instructions=None,
370                       formNamespace=None, fields=None):
371        self.formType = formType
372        self.title = title
373        self.instructions = instructions or []
374        self.formNamespace = formNamespace
375
376        self.fieldList = []
377        self.fields = {}
378
379        if fields:
380            for field in fields:
381                self.addField(field)
382
383    def __repr__(self):
384        r = ["Form(formType=", repr(self.formType)]
385
386        if self.title:
387            r.append(", title=")
388            r.append(repr(self.title))
389        if self.instructions:
390            r.append(", instructions=")
391            r.append(repr(self.instructions))
392        if self.formNamespace:
393            r.append(", formNamespace=")
394            r.append(repr(self.formNamespace))
395        if self.fields:
396            r.append(", fields=")
397            r.append(repr(self.fieldList))
398        r.append(")")
399        return u"".join(r)
400
401
402    def addField(self, field):
403        """
404        Add a field to this form.
405
406        Fields are added in order, and L{fields} is a dictionary of the
407        named fields, that is kept in sync only if this method is used for
408        adding new fields. Multiple fields with the same name are disallowed.
409        """
410        if field.var is not None:
411            if field.var in self.fields:
412                raise Error("Duplicate field %r" % field.var)
413
414            self.fields[field.var] = field
415
416        self.fieldList.append(field)
417
418
419    def toElement(self):
420        form = domish.Element((NS_X_DATA, 'x'))
421        form['type'] = self.formType
422
423        if self.title:
424            form.addElement('title', content=self.title)
425
426        for instruction in self.instructions:
427            form.addElement('instruction', content=instruction)
428
429        if self.formNamespace is not None:
430            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
431            form.addChild(field.toElement())
432
433        for field in self.fieldList:
434            form.addChild(field.toElement(self.formType=='form'))
435
436        return form
437
438
439    @staticmethod
440    def _parse_title(form, element):
441        title = unicode(element)
442        if title:
443            form.title = title
444
445
446    @staticmethod
447    def _parse_instructions(form, element):
448        instructions = unicode(element)
449        if instructions:
450            form.instructions.append(instructions)
451
452
453    @staticmethod
454    def _parse_field(form, element):
455        field = Field.fromElement(element)
456        if (field.var == "FORM_TYPE" and
457            field.fieldType == 'hidden' and
458            field.value):
459            form.formNamespace = field.value
460        else:
461            form.addField(field)
462
463    @staticmethod
464    def fromElement(element):
465        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
466            raise Error("Element provided is not a Data Form")
467
468        form = Form(element.getAttribute("type"))
469
470        for child in element.elements():
471            if child.uri != NS_X_DATA:
472                continue
473
474            func = getattr(Form, '_parse_' + child.name, None)
475            if func:
476                func(form, child)
477
478        return form
479
480    def getValues(self):
481        values = {}
482
483        for name, field in self.fields.iteritems():
484            if len(field.values) > 1:
485                value = field.values
486            else:
487                value = field.value
488
489            values[name] = value
490
491        return values
Note: See TracBrowser for help on using the repository browser.