source: wokkel/data_form.py @ 96:8e6130587088

Last change on this file since 96:8e6130587088 was 96:8e6130587088, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Remove copyright dates from individual source files, only update LICENSE.

  • Property exe set to *
File size: 20.0 KB
Line 
1# -*- test-case-name: wokkel.test.test_data_form -*-
2#
3# Copyright (c) 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
80    @staticmethod
81    def fromElement(element):
82        valueElements = list(domish.generateElementsQNamed(element.children,
83                                                           'value', NS_X_DATA))
84        if not valueElements:
85            raise Error("Option has no value")
86
87        label = element.getAttribute('label')
88        return Option(unicode(valueElements[0]), label)
89
90
91class Field(object):
92    """
93    Data Forms field.
94
95    @ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'},
96                     C{'hidden'}, C{'jid-multi'}, C{'jid-single'},
97                     C{'list-multi'}, {'list-single'}, C{'text-multi'},
98                     C{'text-private'}, C{'text-single'}.
99
100                     The default is C{'text-single'}.
101    @type fieldType: C{str}
102    @ivar var: Field name. Optional if L{fieldType} is C{'fixed'}.
103    @type var: C{str}
104    @ivar label: Human readable label for this field.
105    @type label: C{unicode}
106    @ivar values: The values for this field, for multi-valued field
107                  types, as a list of C{bool}, C{unicode} or L{JID}.
108    @type values: C{list}
109    @ivar options: List of possible values to choose from in a response
110                   to this form as a list of L{Option}s.
111    @type options: C{list}.
112    @ivar desc: Human readable description for this field.
113    @type desc: C{unicode}
114    @ivar required: Whether the field is required to be provided in a
115                    response to this form.
116    @type required: C{bool}.
117    """
118
119    def __init__(self, fieldType='text-single', var=None, value=None,
120                       values=None, options=None, label=None, desc=None,
121                       required=False):
122        """
123        Initialize this field.
124
125        See the identically named instance variables for descriptions.
126
127        If C{value} is not C{None}, it overrides C{values}, setting the
128        given value as the only value for this field.
129        """
130
131        self.fieldType = fieldType
132        self.var = var
133
134        if value is not None:
135            self.value = value
136        else:
137            self.values = values or []
138
139        self.label = label
140
141        try:
142            self.options = [Option(value, label)
143                            for value, label in options.iteritems()]
144        except AttributeError:
145            self.options = options or []
146
147        self.desc = desc
148        self.required = required
149
150
151    def __repr__(self):
152        r = ["Field(fieldType=", repr(self.fieldType)]
153        if self.var:
154            r.append(", var=")
155            r.append(repr(self.var))
156        if self.label:
157            r.append(", label=")
158            r.append(repr(self.label))
159        if self.desc:
160            r.append(", desc=")
161            r.append(repr(self.desc))
162        if self.required:
163            r.append(", required=")
164            r.append(repr(self.required))
165        if self.values:
166            r.append(", values=")
167            r.append(repr(self.values))
168        if self.options:
169            r.append(", options=")
170            r.append(repr(self.options))
171        r.append(")")
172        return u"".join(r)
173
174
175    def __value_set(self, value):
176        """
177        Setter of value property.
178
179        Sets C{value} as the only element of L{values}.
180
181        @type value: C{bool}, C{unicode} or L{JID}
182        """
183        self.values = [value]
184
185
186    def __value_get(self):
187        """
188        Getter of value property.
189
190        Returns the first element of L{values}, if present, or C{None}.
191        """
192
193        if self.values:
194            return self.values[0]
195        else:
196            return None
197
198
199    value = property(__value_get, __value_set, doc="""
200            The value for this field, for single-valued field types.
201
202            This is a special property accessing L{values}.  Writing to this
203            property empties L{values} and then sets the given value as the
204            only element of L{values}.  Reading from this propery returns the
205            first element of L{values}.
206            """)
207
208
209    def typeCheck(self):
210        """
211        Check field properties agains the set field type.
212        """
213        if self.var is None and self.fieldType != 'fixed':
214            raise FieldNameRequiredError()
215
216        if self.values:
217            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
218                                       'text-multi', None) and
219                len(self.values) > 1):
220                raise TooManyValuesError()
221
222            newValues = []
223            for value in self.values:
224                if self.fieldType == 'boolean':
225                    if isinstance(value, (str, unicode)):
226                        checkValue = value.lower()
227                        if not checkValue in ('0', '1', 'false', 'true'):
228                            raise ValueError("Not a boolean")
229                        value = checkValue in ('1', 'true')
230                    value = bool(value)
231                elif self.fieldType in ('jid-single', 'jid-multi'):
232                    if not hasattr(value, 'full'):
233                        value = JID(value)
234
235                newValues.append(value)
236
237            self.values = newValues
238
239
240    def toElement(self, asForm=False):
241        """
242        Return the DOM representation of this Field.
243
244        @rtype: L{domish.Element}.
245        """
246
247        self.typeCheck()
248
249        field = domish.Element((NS_X_DATA, 'field'))
250
251        if self.fieldType:
252            field['type'] = self.fieldType
253
254        if self.var is not None:
255            field['var'] = self.var
256
257        for value in self.values:
258            if isinstance(value, bool):
259                value = unicode(value).lower()
260            else:
261                value = unicode(value)
262
263            field.addElement('value', content=value)
264
265        if asForm:
266            if self.fieldType in ('list-single', 'list-multi'):
267                for option in self.options:
268                    field.addChild(option.toElement())
269
270            if self.label is not None:
271                field['label'] = self.label
272
273            if self.desc is not None:
274                field.addElement('desc', content=self.desc)
275
276            if self.required:
277                field.addElement('required')
278
279        return field
280
281
282    @staticmethod
283    def _parse_desc(field, element):
284        desc = unicode(element)
285        if desc:
286            field.desc = desc
287
288
289    @staticmethod
290    def _parse_option(field, element):
291        field.options.append(Option.fromElement(element))
292
293
294    @staticmethod
295    def _parse_required(field, element):
296        field.required = True
297
298
299    @staticmethod
300    def _parse_value(field, element):
301        value = unicode(element)
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(fieldDict):
330        """
331        Create a field from a dictionary.
332
333        This is a short hand for passing arguments directly on Field object
334        creation. The field type is represented by the C{'type'} key. For
335        C{'options'} the value is not a list of L{Option}s, but a dictionary
336        keyed by value, with an optional label as value.
337        """
338        kwargs = fieldDict.copy()
339
340        if 'type' in fieldDict:
341            kwargs['fieldType'] = fieldDict['type']
342            del kwargs['type']
343
344        if 'options' in fieldDict:
345            options = []
346            for value, label in fieldDict['options'].iteritems():
347                options.append(Option(value, label))
348            kwargs['options'] = options
349
350        return Field(**kwargs)
351
352
353
354class Form(object):
355    """
356    Data Form.
357
358    There are two similarly named properties of forms. The C{formType} is the
359    the so-called type of the form, and is set as the C{'type'} attribute
360    on the form's root element.
361
362    The Field Standardization specification in XEP-0068, defines a way to
363    provide a context for the field names used in this form, by setting a
364    special hidden field named C{'FORM_TYPE'}, to put the names of all
365    other fields in the namespace of the value of that field. This namespace
366    is recorded in the C{formNamespace} instance variable.
367
368    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
369                    or {'result'}.
370    @type formType: C{str}
371
372    @ivar title: Natural language title of the form.
373    @type title: C{unicode}
374
375    @ivar instructions: Natural language instructions as a list of C{unicode}
376        strings without line breaks.
377    @type instructions: C{list}
378
379    @ivar formNamespace: The optional namespace of the field names for this
380        form. This goes in the special field named C{'FORM_TYPE'}, if set.
381    @type formNamespace: C{str}.
382
383    @ivar fields: Dictionary of named fields. Note that this is meant to be
384        used for reading, only. One should use L{addField} or L{makeFields} and
385        L{removeField} for adding and removing fields.
386    @type fields: C{dict}
387
388    @ivar fieldList: List of all fields, in the order they are added. Like
389        C{fields}, this is meant to be used for reading, only.
390    @type fieldList: C{list}
391    """
392
393    def __init__(self, formType, title=None, instructions=None,
394                       formNamespace=None, fields=None):
395        self.formType = formType
396        self.title = title
397        self.instructions = instructions or []
398        self.formNamespace = formNamespace
399
400        self.fieldList = []
401        self.fields = {}
402
403        if fields:
404            for field in fields:
405                self.addField(field)
406
407    def __repr__(self):
408        r = ["Form(formType=", repr(self.formType)]
409
410        if self.title:
411            r.append(", title=")
412            r.append(repr(self.title))
413        if self.instructions:
414            r.append(", instructions=")
415            r.append(repr(self.instructions))
416        if self.formNamespace:
417            r.append(", formNamespace=")
418            r.append(repr(self.formNamespace))
419        if self.fieldList:
420            r.append(", fields=")
421            r.append(repr(self.fieldList))
422        r.append(")")
423        return u"".join(r)
424
425
426    def addField(self, field):
427        """
428        Add a field to this form.
429
430        Fields are added in order, and C{fields} is a dictionary of the
431        named fields, that is kept in sync only if this method is used for
432        adding new fields. Multiple fields with the same name are disallowed.
433        """
434        if field.var is not None:
435            if field.var in self.fields:
436                raise Error("Duplicate field %r" % field.var)
437
438            self.fields[field.var] = field
439
440        self.fieldList.append(field)
441
442
443    def removeField(self, field):
444        """
445        Remove a field from this form.
446        """
447        self.fieldList.remove(field)
448
449        if field.var is not None:
450            del self.fields[field.var]
451
452
453    def makeFields(self, values, fieldDefs=None, filterUnknown=True):
454        """
455        Create fields from values and add them to this form.
456
457        This creates fields from a mapping of name to value(s) and adds them to
458        this form. It is typically used for generating outgoing forms.
459
460        If C{fieldDefs} is not C{None}, this is used to fill in
461        additional properties of fields, like the field types, labels and
462        possible options.
463
464        If C{filterUnknown} is C{True} and C{fieldDefs} is not C{None}, fields
465        will only be created from C{values} with a corresponding entry in
466        C{fieldDefs}.
467
468        If the field type is unknown, the field type is C{None}. When the form
469        is rendered using L{toElement}, these fields will have no C{'type'}
470        attribute, and it is up to the receiving party to interpret the values
471        properly (e.g. by knowing about the FORM_TYPE in L{formNamespace} and
472        the field name).
473
474        @param values: Values to create fields from.
475        @type values: C{dict}
476
477        @param fieldDefs: Field definitions as a dictionary. See
478            L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
479        @type fieldDefs: C{dict}
480
481        @param filterUnknown: If C{True}, ignore fields that are not in
482            C{fieldDefs}.
483        @type filterUnknown: C{bool}
484        """
485        for name, value in values.iteritems():
486            fieldDict = {'var': name,
487                         'type': None}
488
489            if fieldDefs is not None:
490                if name in fieldDefs:
491                    fieldDict.update(fieldDefs[name])
492                elif filterUnknown:
493                    continue
494
495            if isinstance(value, list):
496                fieldDict['values'] = value
497            else:
498                fieldDict['value'] = value
499
500            self.addField(Field.fromDict(fieldDict))
501
502
503    def toElement(self):
504        """
505        Return the DOM representation of this Form.
506
507        @rtype: L{domish.Element}
508        """
509        form = domish.Element((NS_X_DATA, 'x'))
510        form['type'] = self.formType
511
512        if self.title:
513            form.addElement('title', content=self.title)
514
515        for instruction in self.instructions:
516            form.addElement('instructions', content=instruction)
517
518        if self.formNamespace is not None:
519            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
520            form.addChild(field.toElement())
521
522        for field in self.fieldList:
523            form.addChild(field.toElement(self.formType=='form'))
524
525        return form
526
527
528    @staticmethod
529    def _parse_title(form, element):
530        title = unicode(element)
531        if title:
532            form.title = title
533
534
535    @staticmethod
536    def _parse_instructions(form, element):
537        instructions = unicode(element)
538        if instructions:
539            form.instructions.append(instructions)
540
541
542    @staticmethod
543    def _parse_field(form, element):
544        field = Field.fromElement(element)
545        if (field.var == "FORM_TYPE" and
546            field.fieldType == 'hidden' and
547            field.value):
548            form.formNamespace = field.value
549        else:
550            form.addField(field)
551
552    @staticmethod
553    def fromElement(element):
554        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
555            raise Error("Element provided is not a Data Form")
556
557        form = Form(element.getAttribute("type"))
558
559        for child in element.elements():
560            if child.uri != NS_X_DATA:
561                continue
562
563            func = getattr(Form, '_parse_' + child.name, None)
564            if func:
565                func(form, child)
566
567        return form
568
569
570    def getValues(self):
571        """
572        Extract values from the named form fields.
573
574        For all named fields, the corresponding value or values are
575        returned in a dictionary keyed by the field name. For multi-value
576        fields, the dictionary value is a list, otherwise a single value.
577
578        If a field has no type, and the field has multiple values, the value of
579        the dictionary entry is the list of values. Otherwise, it will be a
580        single value.
581
582        @rtype: C{dict}
583        """
584        values = {}
585
586        for name, field in self.fields.iteritems():
587            if (field.fieldType in ('jid-multi', 'list-multi', 'text-multi') or
588                (field.fieldType is None and len(field.values) > 1)):
589                value = field.values
590            else:
591                value = field.value
592
593            values[name] = value
594
595        return values
596
597
598    def typeCheck(self, fieldDefs=None, filterUnknown=False):
599        """
600        Check values of fields according to the field definition.
601
602        This method walks all named fields to check their values against their
603        type, and is typically used for forms received from other entities. The
604        field definition in C{fieldDefs} is used to check the field type.
605
606        If C{filterUnknown} is C{True}, fields that are not present in
607        C{fieldDefs} are removed from the form.
608
609        If the field type is C{None} (when not set by the sending entity),
610        the type from the field definitition is used, or C{'text-single'} if
611        that is not set.
612
613        If C{fieldDefs} is None, an empty dictionary is assumed. This is
614        useful for coercing boolean and JID values on forms with type
615        C{'form'}.
616
617        @param fieldDefs: Field definitions as a dictionary. See
618            L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
619        @type fieldDefs: C{dict}
620
621        @param filterUnknown: If C{True}, remove fields that are not in
622            C{fieldDefs}.
623        @type filterUnknown: C{bool}
624        """
625
626        if fieldDefs is None:
627            fieldDefs = {}
628
629        filtered = []
630
631        for name, field in self.fields.iteritems():
632            if name in fieldDefs:
633                fieldDef = fieldDefs[name]
634                if 'type' not in fieldDef:
635                    fieldDef['type'] = 'text-single'
636
637                if field.fieldType is None:
638                    field.fieldType = fieldDef['type']
639                elif field.fieldType != fieldDef['type']:
640                    raise TypeError("Field type for %r is %r, expected %r" %
641                                    (name,
642                                     field.fieldType,
643                                     fieldDef['type']))
644                else:
645                    # Field type is correct
646                    pass
647                field.typeCheck()
648            elif filterUnknown:
649                filtered.append(field)
650            elif field.fieldType is not None:
651                field.typeCheck()
652            else:
653                # Unknown field without type, no checking, no filtering
654                pass
655
656        for field in filtered:
657            self.removeField(field)
658
659
660
661def findForm(element, formNamespace):
662    """
663    Find a Data Form.
664
665    Look for an element that represents a Data Form with the specified
666    form namespace as a child element of the given element.
667    """
668    if not element:
669        return None
670
671    for child in element.elements():
672        if (child.uri, child.name) == ((NS_X_DATA, 'x')):
673            form = Form.fromElement(child)
674
675            if (form.formNamespace == formNamespace or
676                not form.formNamespace and form.formType=='cancel'):
677                return form
678
679    return None
Note: See TracBrowser for help on using the repository browser.