source: wokkel/data_form.py @ 105:5d8f1609aaf3

Last change on this file since 105:5d8f1609aaf3 was 105:5d8f1609aaf3, checked in by Ralph Meijer <ralphm@…>, 10 years ago

Allow wokkel.data_form.Form to be used as a read-only dict.

In [1334124db2fd] wokkel.pubsub.PubSubRequest.options was changed to be a
Form instead of dictionary, which makes existing code using that field to be
incompatible.

To reduce the impact of that change, this change implements various methods in
wokkel.data_form.Form to emulate a read-only dictionary. It now maps the
name of each field to its value, similar to the dictionary returned from
getValues. The latter is now identical to dict(form).

  • * *

Address incompatible change concerning PubSubRequest?.options.

In changeset [1334124db2fd], PubSubRequest.options was changed to hold the
wokkel.data_form.Form that represents the data form sent in the original
request. This makes it easier to type check the form in a step separate from
parsing the request. However, this is an incompatible change.

To remedy this, we move the form to optionsForm and make options a property
that returns the values of the form.

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