source: wokkel/data_form.py

Last change on this file was 196:80e9a80845ba, checked in by Ralph Meijer <ralphm@…>, 5 years ago

imported patch py3-dataforms.patch

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