Ignore:
Timestamp:
Jul 7, 2008, 4:29:02 PM (13 years ago)
Author:
Ralph Meijer <ralphm@…>
Branch:
default
Convert:
svn:b33ecbfc-034c-dc11-8662-000475d9059e/trunk@55
Message:

Refactor Data Forms.

Author: ralphm.
Fixes #13.

This refactoring provides an abstract representation of Forms, Fields and
Options and each of those can be parsed from or unparsed to XML. This change
also simplifies testing in test_pubsub, by allowing the 'received' requests
to be represented as an XML snippit.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • wokkel/data_form.py

    r1 r25  
    1 # Copyright (c) 2003-2007 Ralph Meijer
     1# -*- test-case-name: wokkel.test.test_data_form -*-
     2#
     3# Copyright (c) 2003-2008 Ralph Meijer
    24# See LICENSE for details.
    35
     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
    416from twisted.words.xish import domish
    517
    618NS_X_DATA = 'jabber:x:data'
    719
    8 class Field(domish.Element):
    9     def __init__(self, type='text-single', var=None, label=None,
    10                        value=None, values=[], options={}):
    11         domish.Element.__init__(self, (NS_X_DATA, 'field'))
    12         self['type'] = type
    13         if var is not None:
    14             self['var'] = var
    15         if label is not None:
    16             self['label'] = label
     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    @staticmethod
     80    def fromElement(element):
     81        valueElements = list(domish.generateElementsQNamed(element.children,
     82                                                           'value', NS_X_DATA))
     83        if not valueElements:
     84            raise Error("Option has no value")
     85
     86        label = element.getAttribute('label')
     87        return Option(unicode(valueElements[0]), label)
     88
     89
     90class Field(object):
     91    """
     92    Data Forms field.
     93
     94    @ivar fieldType: Type of this field. One of C{'boolean'}, C{'fixed'},
     95                     C{'hidden'}, C{'jid-multi'}, C{'jid-single'},
     96                     C{'list-multi'}, {'list-single'}, C{'text-multi'},
     97                     C{'text-private'}, C{'text-single'}.
     98
     99                     The default is C{'text-single'}.
     100    @type fieldType: C{str}
     101    @ivar var: Field name. Optional if L{fieldType} is C{'fixed'}.
     102    @type var: C{str}
     103    @ivar label: Human readable label for this field.
     104    @type label: C{unicode}
     105    @ivar values: The values for this field, for multi-valued field
     106                  types, as a list of C{bool}, C{unicode} or L{JID}.
     107    @type values: C{list}
     108    @ivar options: List of possible values to choose from in a response
     109                   to this form as a list of L{Option}s.
     110    @type options: C{list}.
     111    @ivar desc: Human readable description for this field.
     112    @type desc: C{unicode}
     113    @ivar required: Whether the field is required to be provided in a
     114                    response to this form.
     115    @type required: C{bool}.
     116    """
     117
     118    def __init__(self, fieldType='text-single', var=None, value=None,
     119                       values=None, options=None, label=None, desc=None,
     120                       required=False):
     121        """
     122        Initialize this field.
     123
     124        See the identically named instance variables for descriptions.
     125
     126        If C{value} is not C{None}, it overrides C{values}, setting the
     127        given value as the only value for this field.
     128        """
     129
     130        self.fieldType = fieldType
     131        self.var = var
    17132        if value is not None:
    18             self.set_value(value)
     133            self.value = value
    19134        else:
    20             self.set_values(values)
    21         if type in ['list-single', 'list-multi']:
    22             for value, label in options.iteritems():
    23                 self.addChild(Option(value, label))
    24 
    25     def set_value(self, value):
    26         if self['type'] == 'boolean':
    27             value = str(int(bool(value)))
     135            self.values = values or []
     136
     137        try:
     138            self.options = [Option(value, label)
     139                            for value, label in options.iteritems()]
     140        except AttributeError:
     141            self.options = options or []
     142
     143        self.label = label
     144        self.desc = desc
     145        self.required = required
     146
     147
     148    def __repr__(self):
     149        r = ["Field(fieldType=", repr(self.fieldType)]
     150        if self.var:
     151            r.append(", var=")
     152            r.append(repr(self.var))
     153        if self.label:
     154            r.append(", label=")
     155            r.append(repr(self.label))
     156        if self.desc:
     157            r.append(", desc=")
     158            r.append(repr(self.desc))
     159        if self.required:
     160            r.append(", required=")
     161            r.append(repr(self.required))
     162        if self.values:
     163            r.append(", values=")
     164            r.append(repr(self.values))
     165        if self.options:
     166            r.append(", options=")
     167            r.append(repr(self.options))
     168        r.append(")")
     169        return u"".join(r)
     170
     171
     172    def __value_set(self, value):
     173        """
     174        Setter of value property.
     175
     176        Sets C{value} as the only element of L{values}.
     177
     178        @type value: C{bool}, C{unicode} or L{JID}
     179        """
     180        self.values = [value]
     181
     182
     183    def __value_get(self):
     184        """
     185        Getter of value property.
     186
     187        Returns the first element of L{values}, if present, or C{None}.
     188        """
     189
     190        if self.values:
     191            return self.values[0]
    28192        else:
    29             value = str(value)
    30 
    31         value_element = self.value or self.addElement('value')
    32         value_element.children = []
    33         value_element.addContent(value)
    34 
    35     def set_values(self, values):
    36         for value in values:
    37             value = str(value)
    38             self.addElement('value', content=value)
    39 
    40 class Option(domish.Element):
    41     def __init__(self, value, label=None):
    42         domish.Element.__init__(self, (NS_X_DATA, 'option'))
    43         if label is not None:
    44             self['label'] = label
    45         self.addElement('value', content=value)
    46 
    47 class Form(domish.Element):
    48     def __init__(self, type, form_type):
    49         domish.Element.__init__(self, (NS_X_DATA, 'x'),
    50                                 attribs={'type': type})
    51         self.add_field(type='hidden', var='FORM_TYPE', values=[form_type])
    52 
    53     def add_field(self, type='text-single', var=None, label=None,
    54                         value=None, values=[], options={}):
    55         self.addChild(Field(type, var, label, value, values, options))
     193            return None
     194
     195
     196    value = property(__value_get, __value_set, doc="""
     197            The value for this field, for single-valued field types.
     198
     199            This is a special property accessing L{values}.  Writing to this
     200            property empties L{values} and then sets the given value as the
     201            only element of L{values}.  Reading from this propery returns the
     202            first element of L{values}.
     203            """)
     204
     205
     206    def toElement(self):
     207        """
     208        Return the DOM representation of this Field.
     209
     210        @rtype L{domish.Element}.
     211        """
     212        if self.var is None and self.fieldType != 'fixed':
     213            raise FieldNameRequiredError()
     214
     215        field = domish.Element((NS_X_DATA, 'field'))
     216        field['type'] = self.fieldType
     217
     218        if self.var is not None:
     219            field['var'] = self.var
     220
     221        if self.values:
     222            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
     223                                 'text-multi') and
     224                len(self.values) > 1):
     225                raise TooManyValuesError()
     226
     227            for value in self.values:
     228                if self.fieldType == 'boolean':
     229                    # We send out the textual representation of boolean values
     230                    value = unicode(bool(value)).lower()
     231                elif self.fieldType in ('jid-single', 'jid-multi'):
     232                    value = value.full()
     233
     234                field.addElement('value', content=value)
     235
     236        if self.fieldType in ('list-single', 'list-multi'):
     237            for option in self.options:
     238                field.addChild(option.toElement())
     239
     240        if self.label is not None:
     241            field['label'] = self.label
     242
     243        if self.desc is not None:
     244            field.addElement('desc', content=self.desc)
     245
     246        if self.required:
     247            field.addElement('required')
     248
     249        return field
     250
     251
     252    @staticmethod
     253    def _parse_desc(field, element):
     254        desc = unicode(element)
     255        if desc:
     256            field.desc = desc
     257
     258
     259    @staticmethod
     260    def _parse_option(field, element):
     261        field.options.append(Option.fromElement(element))
     262
     263
     264    @staticmethod
     265    def _parse_required(field, element):
     266        field.required = True
     267
     268
     269    @staticmethod
     270    def _parse_value(field, element):
     271        value = unicode(element)
     272        if field.fieldType == 'boolean':
     273            value = value.lower() in ('1', 'true')
     274        elif field.fieldType in ('jid-multi', 'jid-single'):
     275            value = JID(value)
     276        field.values.append(value)
     277
     278
     279    @staticmethod
     280    def fromElement(element):
     281        field = Field(None)
     282
     283        for eAttr, fAttr in {'type': 'fieldType',
     284                             'var': 'var',
     285                             'label': 'label'}.iteritems():
     286            value = element.getAttribute(eAttr)
     287            if value:
     288                setattr(field, fAttr, value)
     289
     290
     291        for child in element.elements():
     292            if child.uri != NS_X_DATA:
     293                continue
     294
     295            func = getattr(Field, '_parse_' + child.name, None)
     296            if func:
     297                func(field, child)
     298
     299        return field
     300
     301
     302    @staticmethod
     303    def fromDict(dictionary):
     304        if 'type' in dictionary:
     305            dictionary['fieldType'] = dictionary['type']
     306            del dictionary['type']
     307        if 'options' in dictionary:
     308            options = []
     309            for value, label in dictionary['options'].iteritems():
     310                options.append(Option(value, label))
     311            dictionary['options'] = options
     312        return Field(**dictionary)
     313
     314
     315
     316class Form(object):
     317    """
     318    Data Form.
     319
     320    There are two similarly named properties of forms. The L{formType} is the
     321    the so-called type of the form, and is set as the C{'type'} attribute
     322    on the form's root element.
     323
     324    The Field Standardization specification in XEP-0068, defines a way to
     325    provide a context for the field names used in this form, by setting a
     326    special hidden field named C{'FORM_TYPE'}, to put the names of all
     327    other fields in the namespace of the value of that field. This namespace
     328    is recorded in the L{formNamespace} instance variable.
     329
     330    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
     331                    or {'result'}.
     332    @type formType: C{str}.
     333    @ivar formNamespace: The optional namespace of the field names for this
     334                         form. This goes in the special field named
     335                         C{'FORM_TYPE'}, if set.
     336    @type formNamespace: C{str}.
     337    """
     338
     339    def __init__(self, formType, title=None, instructions=None,
     340                       formNamespace=None, fields=None):
     341        self.formType = formType
     342        self.title = title
     343        self.instructions = instructions or []
     344        self.formNamespace = formNamespace
     345        self.fields = fields or []
     346
     347
     348    def __repr__(self):
     349        r = ["Form(formType=", repr(self.formType)]
     350
     351        if self.title:
     352            r.append(", title=")
     353            r.append(repr(self.title))
     354        if self.instructions:
     355            r.append(", instructions=")
     356            r.append(repr(self.instructions))
     357        if self.formNamespace:
     358            r.append(", formNamespace=")
     359            r.append(repr(self.formNamespace))
     360        if self.fields:
     361            r.append(", fields=")
     362            r.append(repr(self.fields))
     363        r.append(")")
     364        return u"".join(r)
     365
     366
     367    def toElement(self):
     368        form = domish.Element((NS_X_DATA, 'x'))
     369        form['type'] = self.formType
     370
     371        if self.title:
     372            form.addElement('title', content=self.title)
     373
     374        for instruction in self.instructions:
     375            form.addElement('instruction', content=instruction)
     376
     377        if self.formNamespace is not None:
     378            field = Field('hidden', 'FORM_TYPE', self.formNamespace)
     379            form.addChild(field.toElement())
     380
     381        for field in self.fields:
     382            form.addChild(field.toElement())
     383
     384        return form
     385
     386
     387    @staticmethod
     388    def _parse_title(form, element):
     389        title = unicode(element)
     390        if title:
     391            form.title = title
     392
     393
     394    @staticmethod
     395    def _parse_instructions(form, element):
     396        instructions = unicode(element)
     397        if instructions:
     398            form.instructions.append(instructions)
     399
     400
     401    @staticmethod
     402    def _parse_field(form, element):
     403        field = Field.fromElement(element)
     404        if (field.var == "FORM_TYPE" and
     405            field.fieldType == 'hidden' and
     406            field.value):
     407            form.formNamespace = field.value
     408        else:
     409            form.fields.append(field)
     410
     411    @staticmethod
     412    def fromElement(element):
     413        if (element.uri, element.name) != ((NS_X_DATA, 'x')):
     414            raise Error("Element provided is not a Data Form")
     415
     416        form = Form(element.getAttribute("type"))
     417
     418        for child in element.elements():
     419            if child.uri != NS_X_DATA:
     420                continue
     421
     422            func = getattr(Form, '_parse_' + child.name, None)
     423            if func:
     424                func(form, child)
     425
     426        return form
     427
     428    def getValues(self):
     429        values = {}
     430
     431        for field in self.fields:
     432            if len(field.values) > 1:
     433                value = field.values
     434            else:
     435                value = field.value
     436
     437            if field.var:
     438                values[field.var] = value
     439
     440        return values
Note: See TracChangeset for help on using the changeset viewer.