source: wokkel/data_form.py @ 48:190498075c7f

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

Fix some epytext problems.

Author: ralphm.

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