source: wokkel/data_form.py @ 80:3ee3e922549d

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

Move findForm to wokkel.data_form

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