Changeset 25:fd00a744a458


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.

Location:
wokkel
Files:
4 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
  • wokkel/iwokkel.py

    r24 r25  
    390390        """
    391391
     392    def getConfigurationOptions():
     393        """
     394        Retrieve all known node configuration options.
     395
     396        The returned dictionary holds the possible node configuration options
     397        by option name. The value of each entry represents the specifics for
     398        that option in a dictionary:
     399
     400        - C{'type'} (C{str}): The option's type (see
     401          L{Field<wokkel.data_form.Field>}'s doc string for possible values).
     402        - C{'label'} (C{unicode}): A human readable label for this option.
     403        - C{'options'} (C{dict}): Optional list of possible values for this
     404          option.
     405
     406        Example::
     407
     408            {
     409            "pubsub#persist_items":
     410                {"type": "boolean",
     411                 "label": "Persist items to storage"},
     412            "pubsub#deliver_payloads":
     413                {"type": "boolean",
     414                 "label": "Deliver payloads with event notifications"},
     415            "pubsub#send_last_published_item":
     416                {"type": "list-single",
     417                 "label": "When to send the last published item",
     418                 "options": {
     419                     "never": "Never",
     420                     "on_sub": "When a new subscription is processed"}
     421                }
     422            }
     423
     424        @rtype: C{dict}.
     425        """
     426
    392427    def getDefaultConfiguration(requestor, service):
    393428        """
  • wokkel/pubsub.py

    r24 r25  
    174174        self.command = self.pubsub.addElement(verb)
    175175
     176
    176177    def send(self, to):
    177178        """
     
    199200        self.xmlstream.addObserver('/message/event[@xmlns="%s"]' %
    200201                                   NS_PUBSUB_EVENT, self._onEvent)
     202
    201203
    202204    def _onEvent(self, message):
     
    271273        return request.send(service).addCallback(cb)
    272274
     275
    273276    def deleteNode(self, service, nodeIdentifier):
    274277        """
     
    283286        request.command['node'] = nodeIdentifier
    284287        return request.send(service)
     288
    285289
    286290    def subscribe(self, service, nodeIdentifier, subscriber):
     
    315319        return request.send(service).addCallback(cb)
    316320
     321
    317322    def unsubscribe(self, service, nodeIdentifier, subscriber):
    318323        """
     
    331336        return request.send(service)
    332337
     338
    333339    def publish(self, service, nodeIdentifier, items=None):
    334340        """
     
    349355
    350356        return request.send(service)
     357
    351358
    352359    def items(self, service, nodeIdentifier, maxItems=None):
     
    431438            }
    432439
     440
    433441    def __init__(self):
    434442        self.discoIdentity = {'category': 'pubsub',
     
    437445
    438446        self.pubSubFeatures = []
     447
    439448
    440449    def connectionMade(self):
     
    444453        self.xmlstream.addObserver(PUBSUB_OWNER_SET, self.handleRequest)
    445454
     455
    446456    def getDiscoInfo(self, requestor, target, nodeIdentifier):
    447457        info = []
     
    454464                         for feature in self.pubSubFeatures])
    455465
    456             return defer.succeed(info)
    457         else:
    458             def toInfo(nodeInfo):
    459                 if not nodeInfo:
    460                     return []
    461 
    462                 (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
    463                 info.append(disco.DiscoIdentity('pubsub', nodeType))
    464                 if metaData:
    465                     form = data_form.Form(type="result",
    466                                           form_type=NS_PUBSUB_META_DATA)
    467                     form.add_field("text-single",
    468                                    "pubsub#node_type",
    469                                    "The type of node (collection or leaf)",
    470                                    nodeType)
    471 
    472                     for metaDatum in metaData:
    473                         form.add_field(**metaDatum)
    474 
    475                     info.append(form)
    476                 return info
    477 
    478             d = self.getNodeInfo(requestor, target, nodeIdentifier)
    479             d.addCallback(toInfo)
    480             return d
     466        def toInfo(nodeInfo):
     467            if not nodeInfo:
     468                return
     469
     470            (nodeType, metaData) = nodeInfo['type'], nodeInfo['meta-data']
     471            info.append(disco.DiscoIdentity('pubsub', nodeType))
     472            if metaData:
     473                form = data_form.Form(formType="result",
     474                                      formNamespace=NS_PUBSUB_META_DATA)
     475                form.fields.append(
     476                        data_form.Field(
     477                            var='pubsub#node_type',
     478                            value=nodeType,
     479                            label='The type of node (collection or leaf)'
     480                        )
     481                )
     482
     483                for metaDatum in metaData:
     484                    form.fields.append(data_form.Field.fromDict(metaDatum))
     485
     486                info.append(form.toElement())
     487
     488        d = self.getNodeInfo(requestor, target, nodeIdentifier or '')
     489        d.addCallback(toInfo)
     490        d.addBoth(lambda result: info)
     491        return d
     492
    481493
    482494    def getDiscoItems(self, requestor, target, nodeIdentifier):
     
    489501        return d
    490502
     503
     504    def _findForm(self, element, formNamespace):
     505        if not element:
     506            return None
     507
     508        form = None
     509        for child in element.elements():
     510            try:
     511                form = data_form.Form.fromElement(child)
     512            except data_form.Error:
     513                continue
     514
     515            if form.formNamespace != NS_PUBSUB_NODE_CONFIG:
     516                continue
     517
     518        return form
     519
     520
    491521    def _onPublish(self, iq):
    492522        requestor = jid.internJID(iq["from"]).userhostJID()
     
    504534
    505535        return self.publish(requestor, service, nodeIdentifier, items)
     536
    506537
    507538    def _onSubscribe(self, iq):
     
    528559        return d
    529560
     561
    530562    def _onUnsubscribe(self, iq):
    531563        requestor = jid.internJID(iq["from"]).userhostJID()
     
    540572        return self.unsubscribe(requestor, service, nodeIdentifier, subscriber)
    541573
     574
    542575    def _onOptionsGet(self, iq):
    543576        raise Unsupported('subscription-options-unavailable')
    544577
     578
    545579    def _onOptionsSet(self, iq):
    546580        raise Unsupported('subscription-options-unavailable')
     581
    547582
    548583    def _onSubscriptions(self, iq):
     
    564599        return d
    565600
     601
    566602    def _onAffiliations(self, iq):
    567603        requestor = jid.internJID(iq["from"]).userhostJID()
     
    582618        d.addCallback(toResponse)
    583619        return d
     620
    584621
    585622    def _onCreate(self, iq):
     
    601638        return d
    602639
    603     def _formFromConfiguration(self, options):
    604         form = data_form.Form(type="form", form_type=NS_PUBSUB_NODE_CONFIG)
    605 
    606         for option in options:
    607             form.add_field(**option)
     640
     641    def _formFromConfiguration(self, values):
     642        options = self.getConfigurationOptions()
     643        form = data_form.Form(formType="form",
     644                              formNamespace=NS_PUBSUB_NODE_CONFIG)
     645
     646        for name, value in values.iteritems():
     647            if name in options:
     648                option = {'var': name}
     649                option.update(options[name])
     650                if isinstance(value, list):
     651                    option['values'] = value
     652                else:
     653                    option['value'] = value
     654                form.fields.append(data_form.Field.fromDict(option))
    608655
    609656        return form
     657
    610658
    611659    def _onDefault(self, iq):
     
    616664            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    617665            default = response.addElement("default")
    618             default.addChild(self._formFromConfiguration(options))
     666            default.addChild(self._formFromConfiguration(options).toElement())
    619667            return response
    620668
     
    623671        return d
    624672
     673
    625674    def _onConfigureGet(self, iq):
    626675        requestor = jid.internJID(iq["from"]).userhostJID()
     
    631680            response = domish.Element((NS_PUBSUB_OWNER, "pubsub"))
    632681            configure = response.addElement("configure")
    633             configure.addChild(self._formFromConfiguration(options))
     682            configure.addChild(self._formFromConfiguration(options).toElement())
    634683
    635684            if nodeIdentifier:
     
    642691        return d
    643692
     693
    644694    def _onConfigureSet(self, iq):
    645695        requestor = jid.internJID(iq["from"]).userhostJID()
     
    647697        nodeIdentifier = iq.pubsub.configure["node"]
    648698
    649         def getFormOptions(form):
    650             options = {}
    651 
    652             for element in form.elements():
    653                 if element.name == 'field' and \
    654                    element.uri == data_form.NS_X_DATA:
    655                     try:
    656                         options[element["var"]] = str(element.value)
    657                     except (KeyError, AttributeError):
    658                         raise BadRequest
    659 
    660             return options
    661 
    662699        # Search configuration form with correct FORM_TYPE and process it
    663700
    664         for element in iq.pubsub.configure.elements():
    665             if element.name != 'x' or element.uri != data_form.NS_X_DATA:
    666                 continue
    667 
    668             type = element.getAttribute("type")
    669             if type == "cancel":
    670                 return None
    671             elif type != "submit":
    672                 continue
    673 
    674             options = getFormOptions(element)
    675 
    676             if options["FORM_TYPE"] == NS_PUBSUB + "#node_config":
    677                 del options["FORM_TYPE"]
     701        form = self._findForm(iq.pubsub.configure, NS_PUBSUB_NODE_CONFIG)
     702
     703        if form:
     704            if form.formType == 'submit':
     705                options = form.getValues()
     706
    678707                return self.setConfiguration(requestor, service,
    679708                                             nodeIdentifier, options)
    680 
    681         raise BadRequest
     709            elif form.formType == 'cancel':
     710                return None
     711
     712        raise BadRequest()
     713
    682714
    683715    def _onItems(self, iq):
     
    721753        return d
    722754
     755
    723756    def _onRetract(self, iq):
    724757        requestor = jid.internJID(iq["from"]).userhostJID()
     
    741774                            itemIdentifiers)
    742775
     776
    743777    def _onPurge(self, iq):
    744778        requestor = jid.internJID(iq["from"]).userhostJID()
     
    752786        return self.purge(requestor, service, nodeIdentifier)
    753787
     788
    754789    def _onDelete(self, iq):
    755790        requestor = jid.internJID(iq["from"]).userhostJID()
     
    763798        return self.delete(requestor, service, nodeIdentifier)
    764799
     800
    765801    def _onAffiliationsGet(self, iq):
    766802        raise Unsupported('modify-affiliations')
    767803
     804
    768805    def _onAffiliationsSet(self, iq):
    769806        raise Unsupported('modify-affiliations')
    770807
     808
    771809    def _onSubscriptionsGet(self, iq):
    772810        raise Unsupported('manage-subscriptions')
     811
    773812
    774813    def _onSubscriptionsSet(self, iq):
     
    788827            self.send(message)
    789828
     829
    790830    def notifyDelete(self, service, nodeIdentifier, recipients):
    791831        for recipient in recipients:
     
    798838            self.send(message)
    799839
     840
    800841    def getNodeInfo(self, requestor, service, nodeIdentifier):
    801842        return None
    802843
     844
    803845    def getNodes(self, requestor, service):
    804846        return []
    805847
     848
    806849    def publish(self, requestor, service, nodeIdentifier, items):
    807850        raise Unsupported('publish')
    808851
     852
    809853    def subscribe(self, requestor, service, nodeIdentifier, subscriber):
    810854        raise Unsupported('subscribe')
    811855
     856
    812857    def unsubscribe(self, requestor, service, nodeIdentifier, subscriber):
    813858        raise Unsupported('subscribe')
    814859
     860
    815861    def subscriptions(self, requestor, service):
    816862        raise Unsupported('retrieve-subscriptions')
    817863
     864
    818865    def affiliations(self, requestor, service):
    819866        raise Unsupported('retrieve-affiliations')
    820867
     868
    821869    def create(self, requestor, service, nodeIdentifier):
    822870        raise Unsupported('create-nodes')
    823871
     872
     873    def getConfigurationOptions(self):
     874        return {}
     875
     876
    824877    def getDefaultConfiguration(self, requestor, service):
    825878        raise Unsupported('retrieve-default')
    826879
     880
    827881    def getConfiguration(self, requestor, service, nodeIdentifier):
    828882        raise Unsupported('config-node')
    829883
     884
    830885    def setConfiguration(self, requestor, service, nodeIdentifier, options):
    831886        raise Unsupported('config-node')
     887
    832888
    833889    def items(self, requestor, service, nodeIdentifier, maxItems,
     
    835891        raise Unsupported('retrieve-items')
    836892
     893
    837894    def retract(self, requestor, service, nodeIdentifier, itemIdentifiers):
    838895        raise Unsupported('retract-items')
    839896
     897
    840898    def purge(self, requestor, service, nodeIdentifier):
    841899        raise Unsupported('purge-nodes')
    842900
     901
    843902    def delete(self, requestor, service, nodeIdentifier):
    844903        raise Unsupported('delete-nodes')
  • wokkel/test/test_pubsub.py

    r24 r25  
    55Tests for L{wokkel.pubsub}
    66"""
     7
    78from zope.interface import verify
    89
     
    1314from twisted.words.protocols.jabber.jid import JID
    1415
    15 from wokkel import iwokkel, pubsub
     16from wokkel import data_form, iwokkel, pubsub
    1617from wokkel.generic import parseXml
    1718from wokkel.test.helpers import XmlStreamStub
     
    2324
    2425NS_PUBSUB = 'http://jabber.org/protocol/pubsub'
     26NS_PUBSUB_CONFIG = 'http://jabber.org/protocol/pubsub#node_config'
    2527NS_PUBSUB_ERRORS = 'http://jabber.org/protocol/pubsub#errors'
    2628NS_PUBSUB_EVENT = 'http://jabber.org/protocol/pubsub#event'
     29NS_PUBSUB_OWNER = 'http://jabber.org/protocol/pubsub#owner'
    2730
    2831def calledAsync(fn):
     
    445448    """
    446449
    447     def handleRequest(self, handler, iq):
     450    def setUp(self):
     451        self.service = pubsub.PubSubService()
     452
     453    def handleRequest(self, xml):
    448454        """
    449455        Find a handler and call it directly
    450456        """
    451         for queryString, method in handler.iqHandlers.iteritems():
     457        handler = None
     458        iq = parseXml(xml)
     459        for queryString, method in self.service.iqHandlers.iteritems():
    452460            if xpath.internQuery(queryString).matches(iq):
    453                 handler = getattr(handler, method)
     461                handler = getattr(self.service, method)
    454462
    455463        if handler:
     
    465473        Do instances of L{pubsub.PubSubService} provide L{iwokkel.IPubSubService}?
    466474        """
    467         verify.verifyObject(iwokkel.IPubSubService, pubsub.PubSubService())
     475        verify.verifyObject(iwokkel.IPubSubService, self.service)
    468476
    469477
     
    484492            self.assertEquals('bad-request', result.condition)
    485493
    486         handler = pubsub.PubSubService()
    487         d = self.handleRequest(handler, parseXml(xml))
     494        d = self.handleRequest(xml)
    488495        self.assertFailure(d, error.StanzaError)
    489496        d.addCallback(cb)
    490497        return d
     498
    491499
    492500    def test_onPublish(self):
     
    495503        called.
    496504        """
    497         class Handler(pubsub.PubSubService):
    498             def publish(self, requestor, service, nodeIdentifier, items):
    499                 return defer.succeed((requestor, service,nodeIdentifier,
    500                                       items))
    501505
    502506        xml = """
     
    509513        """
    510514
    511         def cb(result):
    512             self.assertEqual((JID('user@example.org'),
    513                               JID('pubsub.example.org'),'test', []), result)
    514 
    515         handler = Handler()
    516         d = self.handleRequest(handler, parseXml(xml))
    517         d.addCallback(cb)
    518         return d
     515        def publish(requestor, service, nodeIdentifier, items):
     516            self.assertEqual(JID('user@example.org'), requestor)
     517            self.assertEqual(JID('pubsub.example.org'), service)
     518            self.assertEqual('test', nodeIdentifier)
     519            self.assertEqual([], items)
     520            return defer.succeed(None)
     521
     522        self.service.publish = publish
     523        return self.handleRequest(xml)
    519524
    520525
     
    524529        """
    525530
    526         handler = pubsub.PubSubService()
    527531        xml = """
    528532        <iq type='get' to='pubsub.example.org'
     
    539543            self.assertEquals(NS_PUBSUB_ERRORS, result.appCondition.uri)
    540544
    541         handler = pubsub.PubSubService()
    542         d = self.handleRequest(handler, parseXml(xml))
     545        d = self.handleRequest(xml)
    543546        self.assertFailure(d, error.StanzaError)
    544547        d.addCallback(cb)
     
    546549
    547550
     551    def test_onDefault(self):
     552        """
     553        A default request should result in
     554        L{PubSubService.getDefaultConfiguration} being called.
     555        """
     556
     557        xml = """
     558        <iq type='get' to='pubsub.example.org'
     559                       from='user@example.org'>
     560          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     561            <default/>
     562          </pubsub>
     563        </iq>
     564        """
     565
     566        def getConfigurationOptions():
     567            return {
     568                "pubsub#persist_items":
     569                    {"type": "boolean",
     570                     "label": "Persist items to storage"},
     571                "pubsub#deliver_payloads":
     572                    {"type": "boolean",
     573                     "label": "Deliver payloads with event notifications"}
     574                }
     575
     576        def getDefaultConfiguration(requestor, service):
     577            self.assertEqual(JID('user@example.org'), requestor)
     578            self.assertEqual(JID('pubsub.example.org'), service)
     579            return defer.succeed({})
     580
     581        def cb(element):
     582            self.assertEqual('pubsub', element.name)
     583            self.assertEqual(NS_PUBSUB_OWNER, element.uri)
     584            self.assertEqual(NS_PUBSUB_OWNER, element.default.uri)
     585            form = data_form.Form.fromElement(element.default.x)
     586            self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
     587
     588        self.service.getConfigurationOptions = getConfigurationOptions
     589        self.service.getDefaultConfiguration = getDefaultConfiguration
     590        d = self.handleRequest(xml)
     591        d.addCallback(cb)
     592        return d
     593
     594
     595    def test_onConfigureGet(self):
     596        """
     597        On a node configuration get request L{PubSubService.getConfiguration}
     598        is called and results in a data form with the configuration.
     599        """
     600
     601        xml = """
     602        <iq type='get' to='pubsub.example.org'
     603                       from='user@example.org'>
     604          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     605            <configure node='test'/>
     606          </pubsub>
     607        </iq>
     608        """
     609
     610        def getConfigurationOptions():
     611            return {
     612                "pubsub#persist_items":
     613                    {"type": "boolean",
     614                     "label": "Persist items to storage"},
     615                "pubsub#deliver_payloads":
     616                    {"type": "boolean",
     617                     "label": "Deliver payloads with event notifications"}
     618                }
     619
     620        def getConfiguration(requestor, service, nodeIdentifier):
     621            self.assertEqual(JID('user@example.org'), requestor)
     622            self.assertEqual(JID('pubsub.example.org'), service)
     623            self.assertEqual('test', nodeIdentifier)
     624
     625            return defer.succeed({'pubsub#deliver_payloads': '0',
     626                                  'pubsub#persist_items': '1'})
     627
     628        def cb(element):
     629            self.assertEqual('pubsub', element.name)
     630            self.assertEqual(NS_PUBSUB_OWNER, element.uri)
     631            self.assertEqual(NS_PUBSUB_OWNER, element.configure.uri)
     632            form = data_form.Form.fromElement(element.configure.x)
     633            self.assertEqual(NS_PUBSUB_CONFIG, form.formNamespace)
     634            fields = dict([(field.var, field) for field in form.fields])
     635
     636            self.assertIn('pubsub#deliver_payloads', fields)
     637            field = fields['pubsub#deliver_payloads']
     638            self.assertEqual('boolean', field.fieldType)
     639            self.assertEqual(True, field.value)
     640
     641            self.assertIn('pubsub#persist_items', fields)
     642            field = fields['pubsub#persist_items']
     643            self.assertEqual('boolean', field.fieldType)
     644            self.assertEqual(True, field.value)
     645
     646        self.service.getConfigurationOptions = getConfigurationOptions
     647        self.service.getConfiguration = getConfiguration
     648        d = self.handleRequest(xml)
     649        d.addCallback(cb)
     650        return d
     651
     652
     653    def test_onConfigureSet(self):
     654        """
     655        On a node configuration set request the Data Form is parsed and
     656        L{PubSubService.setConfiguration} is called with the passed options.
     657        """
     658
     659        xml = """
     660        <iq type='set' to='pubsub.example.org'
     661                       from='user@example.org'>
     662          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     663            <configure node='test'>
     664              <x xmlns='jabber:x:data' type='submit'>
     665                <field var='FORM_TYPE' type='hidden'>
     666                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     667                </field>
     668                <field var='pubsub#deliver_payloads'><value>0</value></field>
     669                <field var='pubsub#persist_items'><value>1</value></field>
     670              </x>
     671            </configure>
     672          </pubsub>
     673        </iq>
     674        """
     675
     676        def setConfiguration(requestor, service, nodeIdentifier, options):
     677            self.assertEqual(JID('user@example.org'), requestor)
     678            self.assertEqual(JID('pubsub.example.org'), service)
     679            self.assertEqual('test', nodeIdentifier)
     680            self.assertEqual({'pubsub#deliver_payloads': '0',
     681                              'pubsub#persist_items': '1'}, options)
     682            return defer.succeed(None)
     683
     684        self.service.setConfiguration = setConfiguration
     685        return self.handleRequest(xml)
     686
     687
     688    def test_onConfigureSetCancel(self):
     689        """
     690        The node configuration is cancelled, L{PubSubService.setConfiguration}
     691        not called.
     692        """
     693
     694        xml = """
     695        <iq type='set' to='pubsub.example.org'
     696                       from='user@example.org'>
     697          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     698            <configure node='test'>
     699              <x xmlns='jabber:x:data' type='cancel'>
     700                <field var='FORM_TYPE' type='hidden'>
     701                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     702                </field>
     703              </x>
     704            </configure>
     705          </pubsub>
     706        </iq>
     707        """
     708
     709        def setConfiguration(requestor, service, nodeIdentifier, options):
     710            self.fail("Unexpected call to setConfiguration")
     711
     712        self.service.setConfiguration = setConfiguration
     713        return self.handleRequest(xml)
     714
     715
    548716    def test_onItems(self):
    549717        """
    550718        On a items request, return all items for the given node.
    551719        """
    552         class Handler(pubsub.PubSubService):
    553             def items(self, *args, **kwargs):
    554                 self.args = args
    555                 self.kwargs = kwargs
    556                 return defer.succeed([pubsub.Item('current')])
    557 
    558720        xml = """
    559721        <iq type='get' to='pubsub.example.org'
     
    565727        """
    566728
     729        def items(requestor, service, nodeIdentifier, maxItems, items):
     730            self.assertEqual(JID('user@example.org'), requestor)
     731            self.assertEqual(JID('pubsub.example.org'), service)
     732            self.assertEqual('test', nodeIdentifier)
     733            self.assertIdentical(None, maxItems)
     734            self.assertEqual([], items)
     735            return defer.succeed([pubsub.Item('current')])
     736
    567737        def cb(element):
    568             self.assertEqual((JID('user@example.org'),
    569                               JID('pubsub.example.org'), 'test', None, []),
    570                              handler.args)
    571 
    572738            self.assertEqual(NS_PUBSUB, element.uri)
    573739            self.assertEqual(NS_PUBSUB, element.items.uri)
     
    579745            self.assertEqual('current', item['id'])
    580746
    581         handler = Handler()
    582         d = self.handleRequest(handler, parseXml(xml))
    583         d.addCallback(cb)
    584         return d
     747        self.service.items = items
     748        d = self.handleRequest(xml)
     749        d.addCallback(cb)
     750        return d
Note: See TracChangeset for help on using the changeset viewer.