source: wokkel/data_form.py @ 195:ff66e31c8607

Last change on this file since 195:ff66e31c8607 was 182:b9180f126260, checked in by Ralph Meijer <ralphm@…>, 9 years ago

Make pyflakes happy.

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