source: ralphm-patches/pubsub-subscription-options.patch @ 20:9e1c2535b74e

Last change on this file since 20:9e1c2535b74e was 20:9e1c2535b74e, checked in by Ralph Meijer <ralphm@…>, 10 years ago

Add filterUnknown for typeCheck, request.options is a form, per docstring.

File size: 23.6 KB
  • wokkel/data_form.py

    diff -r 3ef9bc7a0d70 wokkel/data_form.py
    a b  
    7676            option['label'] = self.label
    7777        return option
    7878
     79
    7980    @staticmethod
    8081    def fromElement(element):
    8182        valueElements = list(domish.generateElementsQNamed(element.children,
     
    212213
    213214        if self.values:
    214215            if (self.fieldType not in ('hidden', 'jid-multi', 'list-multi',
    215                                  'text-multi') and
     216                                       'text-multi') and
    216217                len(self.values) > 1):
    217218                raise TooManyValuesError()
    218219
     
    233234
    234235            self.values = newValues
    235236
     237
    236238    def toElement(self, asForm=False):
    237239        """
    238240        Return the DOM representation of this Field.
     
    256258            elif self.fieldType in ('jid-single', 'jid-multi'):
    257259                value = value.full()
    258260
    259             field.addElement('value', content=value)
     261            field.addElement('value', content=unicode(value))
    260262
    261263        if asForm:
    262264            if self.fieldType in ('list-single', 'list-multi'):
     
    322324
    323325
    324326    @staticmethod
    325     def fromDict(dictionary):
    326         kwargs = dictionary.copy()
     327    def fromDict(fieldDict):
     328        kwargs = fieldDict.copy()
    327329
    328         if 'type' in dictionary:
    329             kwargs['fieldType'] = dictionary['type']
     330        if 'type' in fieldDict:
     331            kwargs['fieldType'] = fieldDict['type']
    330332            del kwargs['type']
    331333
    332         if 'options' in dictionary:
     334        if 'options' in fieldDict:
    333335            options = []
    334             for value, label in dictionary['options'].iteritems():
     336            for value, label in fieldDict['options'].iteritems():
    335337                options.append(Option(value, label))
    336338            kwargs['options'] = options
    337339
     
    343345    """
    344346    Data Form.
    345347
    346     There are two similarly named properties of forms. The L{formType} is the
     348    There are two similarly named properties of forms. The C{formType} is the
    347349    the so-called type of the form, and is set as the C{'type'} attribute
    348350    on the form's root element.
    349351
     
    351353    provide a context for the field names used in this form, by setting a
    352354    special hidden field named C{'FORM_TYPE'}, to put the names of all
    353355    other fields in the namespace of the value of that field. This namespace
    354     is recorded in the L{formNamespace} instance variable.
     356    is recorded in the C{formNamespace} instance variable.
    355357
    356358    @ivar formType: Type of form. One of C{'form'}, C{'submit'}, {'cancel'},
    357359                    or {'result'}.
    358     @type formType: C{str}.
     360    @type formType: C{str}
     361
    359362    @ivar formNamespace: The optional namespace of the field names for this
    360                          form. This goes in the special field named
    361                          C{'FORM_TYPE'}, if set.
    362     @type formNamespace: C{str}.
    363     @ivar fields: Dictionary of fields that have a name. Note that this is
    364                   meant to be used for reading, only. One should use
    365                   L{addField} for adding fields.
     363        form. This goes in the special field named C{'FORM_TYPE'}, if set.
     364    @type formNamespace: C{str}
     365
     366    @ivar fields: Dictionary of named fields. Note that this is meant to be
     367        used for reading, only. One should use L{addField} or L{makeFields} and
     368        L{removeField} for adding and removing fields.
    366369    @type fields: C{dict}
     370
     371    @ivar fieldList: List of all fields, in the order they are added. Like
     372        C{fields}, this is meant to be used for reading, only.
     373    @type fieldList: C{list}
     374
    367375    """
    368376
    369377    def __init__(self, formType, title=None, instructions=None,
     
    403411        """
    404412        Add a field to this form.
    405413
    406         Fields are added in order, and L{fields} is a dictionary of the
     414        Fields are added in order, and C{fields} is a dictionary of the
    407415        named fields, that is kept in sync only if this method is used for
    408416        adding new fields. Multiple fields with the same name are disallowed.
    409417        """
     
    416424        self.fieldList.append(field)
    417425
    418426
     427    def removeField(self, field):
     428        """
     429        Remove a field from this form.
     430        """
     431        self.fieldList.remove(field)
     432
     433        if field.var is not None:
     434            del self.fields[field.var]
     435
     436
     437    def makeFields(self, values, fieldDefs=None, filterUnknown=True):
     438        """
     439        Create fields from values and add them to this form.
     440
     441        This creates fields from a mapping of name to value(s) and adds them to
     442        this form. If C{fieldDefs} is not C{None}, this is used to fill in
     443        additional properties of fields, like the field types, labels and
     444        possible options. If C{filterUnknown} is C{True} and C{fieldDefs} is
     445        not C{None}, fields will only be created from C{values} with a
     446        corresponding entry in C{fieldDefs}.
     447
     448        @param values: Values to create fields from.
     449        @type values: C{dict}
     450
     451        @param fieldDefs: Field definitions as a dictionary. See
     452            L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
     453        @type fieldDefs: C{dict}
     454
     455        @param filterUnknown: If C{True}, ignore fields that are not in
     456            C{fieldDefs}.
     457        @type filterUnknown: C{bool}
     458        """
     459        for name, value in values.iteritems():
     460            fieldDict = {'var': name}
     461
     462            if fieldDefs is not None:
     463                if name in fieldDefs:
     464                    fieldDict.update(fieldDefs[name])
     465                elif filterUnknown:
     466                    continue
     467
     468            if isinstance(value, list):
     469                fieldDict['values'] = value
     470                if 'type' not in fieldDict:
     471                    if value and hasattr(value[0], 'full'):
     472                        fieldDict['type'] = 'jid-multi'
     473                    else:
     474                        fieldDict['type'] = 'text-multi'
     475            else:
     476                fieldDict['value'] = value
     477                if 'type' not in fieldDict:
     478                    if hasattr(value, 'full'):
     479                        fieldDict['type'] = 'jid-single'
     480                    elif isinstance(value, bool):
     481                        fieldDict['type'] = 'boolean'
     482
     483            self.addField(Field.fromDict(fieldDict))
     484
     485
    419486    def toElement(self):
     487        """
     488        Return the DOM representation of this Form.
     489
     490        @rtype: L{domish.Element}
     491        """
    420492        form = domish.Element((NS_X_DATA, 'x'))
    421493        form['type'] = self.formType
    422494
     
    477549
    478550        return form
    479551
     552
    480553    def getValues(self):
     554        """
     555        Extract values from the named form fields.
     556
     557        For all named fields, the corresponding value or values are
     558        returned in a dictionary keyed by the field name. For multi-value
     559        fields, the dictionary value is a list, otherwise a single value.
     560
     561        @rtype: C{dict}
     562        """
    481563        values = {}
    482564
    483565        for name, field in self.fields.iteritems():
     
    489571            values[name] = value
    490572
    491573        return values
     574
     575
     576    def typeCheck(self, fieldDefs, filterUnknown=False):
     577        """
     578        Check values of fields according to the field definition.
     579
     580        This method walks all named fields to check their values against their
     581        type. The field definition in C{fieldDefs} is used to check the field
     582        type. Unknown fields are ignored.
     583
     584        If the field type is C{None} (when not set by the receiving entity),
     585        C{'text-single'} is assumed, as this is the default value.
     586
     587        @param fieldDefs: Field definitions as a dictionary. See
     588            L{wokkel.iwokkel.IPubSubService.getConfigurationOptions}
     589        @type fieldDefs: C{dict}
     590
     591        @param filterUnknown: If C{True}, remove fields that are not in
     592            C{fieldDefs}.
     593        @type filterUnknown: C{bool}
     594        """
     595
     596        filtered = []
     597
     598        for name, field in self.fields.iteritems():
     599            if name in fieldDefs:
     600                if field.fieldType is None:
     601                    field.fieldType = fieldDefs[name]['type']
     602                elif field.fieldType != fieldDefs[name]['type']:
     603                    raise TypeError("Field type for %r is %r, expected %r" %
     604                                    (name,
     605                                     field.fieldType,
     606                                     fieldDefs[name]['type']))
     607                else:
     608                    # Field type is correct
     609                    pass
     610                field.typeCheck()
     611            elif filterUnknown:
     612                filtered.append(field)
     613            else:
     614                # Unknown field, not filtering
     615                pass
     616
     617        for field in filtered:
     618            self.removeField(field)
  • wokkel/pubsub.py

    diff -r 3ef9bc7a0d70 wokkel/pubsub.py
    a b  
    228228    # Map request verb to parameter handler names
    229229    _parameters = {
    230230        'publish': ['node', 'items'],
    231         'subscribe': ['nodeOrEmpty', 'jid'],
     231        'subscribe': ['nodeOrEmpty', 'jid', 'optionsWithSubscribe'],
    232232        'unsubscribe': ['nodeOrEmpty', 'jid'],
    233233        'optionsGet': ['nodeOrEmpty', 'jid'],
    234234        'optionsSet': ['nodeOrEmpty', 'jid', 'options'],
     
    380380        """
    381381        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_NODE_CONFIG)
    382382        if form:
    383             if form.formType == 'submit':
    384                 self.options = form.getValues()
    385             elif form.formType == 'cancel':
    386                 self.options = {}
     383            if form.formType in ('submit', 'cancel'):
     384                self.options = form
    387385            else:
    388386                raise BadRequest(text="Unexpected form type %r" % form.formType)
    389387        else:
     
    439437    def _parse_options(self, verbElement):
    440438        form = PubSubRequest._findForm(verbElement, NS_PUBSUB_SUBSCRIBE_OPTIONS)
    441439        if form:
    442             if form.formType == 'submit':
    443                 self.options = form.getValues()
    444             elif form.formType == 'cancel':
    445                 self.options = {}
    446             else:
     440            if form.formType not in ('submit', 'cancel'):
    447441                raise BadRequest(text="Unexpected form type %r" % form.formType)
     442            self.options = form
    448443        else:
    449444            raise BadRequest(text="Missing options form")
    450445
     446
     447    def _render_options(self, verbElement):
     448        verbElement.addChild(self.options.toElement())
     449
     450
     451    def _parse_optionsWithSubscribe(self, verbElement):
     452        for element in verbElement.parent.elements():
     453            if element.name == 'options' and element.uri == NS_PUBSUB:
     454                form = PubSubRequest._findForm(element,
     455                                               NS_PUBSUB_SUBSCRIBE_OPTIONS)
     456                if not form:
     457                    continue
     458
     459                self.options = form
     460                if form.formType != 'submit':
     461                    BadRequest(text="Unexpected form type %r" % form.formType)
     462
     463
     464    def _render_optionsWithSubscribe(self, verbElement):
     465        if self.options:
     466            optionsElement = verbElement.parent.addElement('options')
     467            self._render_options(optionsElement)
     468
     469
    451470    def parseElement(self, element):
    452471        """
    453472        Parse the publish-subscribe verb and parameters out of a request.
    454473        """
    455474        generic.Stanza.parseElement(self, element)
    456475
     476        verbs = []
     477        children = []
    457478        for child in element.pubsub.elements():
    458479            key = (self.stanzaType, child.uri, child.name)
    459480            try:
    460481                verb = self._requestVerbMap[key]
    461482            except KeyError:
    462483                continue
    463             else:
    464                 self.verb = verb
    465                 break
    466484
    467         if not self.verb:
     485            verbs.append(verb)
     486            children.append(child)
     487
     488        if not verbs:
    468489            raise NotImplementedError()
    469490
    470         for parameter in self._parameters[verb]:
     491        if len(verbs) > 1:
     492            if 'optionsSet' in verbs and 'subscribe' in verbs:
     493                self.verb = 'subscribe'
     494                child = children[verbs.index('subscribe')]
     495            else:
     496                raise NotImplementedError()
     497        else:
     498            self.verb = verbs[0]
     499
     500        for parameter in self._parameters[self.verb]:
    471501            getattr(self, '_parse_%s' % parameter)(child)
    472502
    473503
     504
    474505    def send(self, xs):
    475506        """
    476507        Send this request to its recipient.
     
    674705        return request.send(self.xmlstream)
    675706
    676707
    677     def subscribe(self, service, nodeIdentifier, subscriber, sender=None):
     708    def subscribe(self, service, nodeIdentifier, subscriber,
     709                        options=None, sender=None):
    678710        """
    679711        Subscribe to a publish subscribe node.
    680712
     
    685717        @param subscriber: The entity to subscribe to the node. This entity
    686718                           will get notifications of new published items.
    687719        @type subscriber: L{JID}
     720        @param options: Subscription options.
     721        @type options: C{dict}.
    688722        """
    689723        request = PubSubRequest('subscribe')
    690724        request.recipient = service
     
    692726        request.subscriber = subscriber
    693727        request.sender = sender
    694728
     729        if options:
     730            form = data_form.Form(formType='result',
     731                                  formNamespace=NS_PUBSUB_SUBSCRIBE_OPTIONS)
     732            form.makeFields(options)
     733            request.options = form
     734
    695735        def cb(iq):
    696736            subscription = iq.pubsub.subscription["subscription"]
    697737
     
    956996            handlerName, argNames = self._legacyHandlers[request.verb]
    957997            handler = getattr(self, handlerName)
    958998            args = [getattr(request, arg) for arg in argNames]
     999            if 'options' in argNames:
     1000                args[argNames.index('options')] = request.options.getValues()
    9591001            d = handler(*args)
    9601002
    9611003        # If needed, translate the result into a response
     
    10121054            return None
    10131055
    10141056
    1015     def _makeFields(self, options, values):
    1016         fields = []
    1017         for name, value in values.iteritems():
    1018             if name not in options:
    1019                 continue
    1020 
    1021             option = {'var': name}
    1022             option.update(options[name])
    1023             if isinstance(value, list):
    1024                 option['values'] = value
    1025             else:
    1026                 option['value'] = value
    1027             fields.append(data_form.Field.fromDict(option))
    1028         return fields
    1029 
    1030 
    10311057    def _formFromConfiguration(self, resource, values):
    1032         options = resource.getConfigurationOptions()
    1033         fields = self._makeFields(options, values)
     1058        fieldDefs = resource.getConfigurationOptions()
    10341059        form = data_form.Form(formType="form",
    1035                               formNamespace=NS_PUBSUB_NODE_CONFIG,
    1036                               fields=fields)
    1037 
     1060                              formNamespace=NS_PUBSUB_NODE_CONFIG)
     1061        form.makeFields(values, fieldDefs)
    10381062        return form
    10391063
    10401064
    1041     def _checkConfiguration(self, resource, values):
    1042         options = resource.getConfigurationOptions()
    1043         processedValues = {}
    1044 
    1045         for key, value in values.iteritems():
    1046             if key not in options:
    1047                 continue
    1048 
    1049             option = {'var': key}
    1050             option.update(options[key])
    1051             field = data_form.Field.fromDict(option)
    1052             if isinstance(value, list):
    1053                 field.values = value
    1054             else:
    1055                 field.value = value
    1056             field.typeCheck()
    1057 
    1058             if isinstance(value, list):
    1059                 processedValues[key] = field.values
    1060             else:
    1061                 processedValues[key] = field.value
    1062 
    1063         return processedValues
     1065    def _checkConfiguration(self, resource, form):
     1066        fieldDefs = resource.getConfigurationOptions()
     1067        form.typeCheck(fieldDefs, filterUnknown=True)
    10641068
    10651069
    10661070    def _preProcess_default(self, resource, request):
     
    10911095
    10921096
    10931097    def _preProcess_configureSet(self, resource, request):
    1094         if request.options:
    1095             request.options = self._checkConfiguration(resource,
    1096                                                        request.options)
     1098        if request.options.formType == 'cancel':
     1099            return None
     1100        else:
     1101            self._checkConfiguration(resource, request.options)
    10971102            return request
    1098         else:
    1099             return None
    11001103
    11011104
    11021105    def _toResponse_items(self, result, resource, request):
  • wokkel/test/test_pubsub.py

    diff -r 3ef9bc7a0d70 wokkel/test/test_pubsub.py
    a b  
    447447        return d
    448448
    449449
     450    def test_subscribeWithOptions(self):
     451        options = {'pubsub#deliver': False}
     452
     453        d = self.protocol.subscribe(JID('pubsub.example.org'), 'test',
     454                                    JID('user@example.org'),
     455                                    options=options)
     456        iq = self.stub.output[-1]
     457
     458        # Check options present
     459        childNames = []
     460        for element in iq.pubsub.elements():
     461            if element.uri == NS_PUBSUB:
     462                childNames.append(element.name)
     463
     464        self.assertEqual(['subscribe', 'options'], childNames)
     465        form = data_form.Form.fromElement(iq.pubsub.options.x)
     466        form.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
     467        self.assertEqual(options, form.getValues())
     468
     469        # Send response
     470        response = toResponse(iq, 'result')
     471        pubsub = response.addElement((NS_PUBSUB, 'pubsub'))
     472        subscription = pubsub.addElement('subscription')
     473        subscription['node'] = 'test'
     474        subscription['jid'] = 'user@example.org'
     475        subscription['subscription'] = 'subscribed'
     476        self.stub.send(response)
     477
     478        return d
     479
     480
    450481    def test_subscribeWithSender(self):
    451482        """
    452483        Test sending subscription request from a specific JID.
     
    718749        self.assertEqual(NS_PUBSUB_ERRORS, err.appCondition.uri)
    719750        self.assertEqual('jid-required', err.appCondition.name)
    720751
     752
     753    def test_fromElementSubscribeWithOptions(self):
     754        """
     755        Test parsing a subscription request.
     756        """
     757
     758        xml = """
     759        <iq type='set' to='pubsub.example.org'
     760                       from='user@example.org'>
     761          <pubsub xmlns='http://jabber.org/protocol/pubsub'>
     762            <subscribe node='test' jid='user@example.org/Home'/>
     763            <options>
     764              <x xmlns="jabber:x:data" type='submit'>
     765                <field var='FORM_TYPE' type='hidden'>
     766                  <value>http://jabber.org/protocol/pubsub#subscribe_options</value>
     767                </field>
     768                <field var='pubsub#deliver' type='boolean'
     769                       label='Enable delivery?'>
     770                  <value>1</value>
     771                </field>
     772              </x>
     773            </options>
     774          </pubsub>
     775        </iq>
     776        """
     777
     778        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
     779        self.assertEqual('subscribe', request.verb)
     780        request.options.typeCheck({'pubsub#deliver': {'type': 'boolean'}})
     781        self.assertEqual({'pubsub#deliver': True}, request.options.getValues())
     782
     783
    721784    def test_fromElementUnsubscribe(self):
    722785        """
    723786        Test parsing an unsubscription request.
     
    805868        self.assertEqual(JID('pubsub.example.org'), request.recipient)
    806869        self.assertEqual('test', request.nodeIdentifier)
    807870        self.assertEqual(JID('user@example.org/Home'), request.subscriber)
    808         self.assertEqual({'pubsub#deliver': '1'}, request.options)
     871        self.assertEqual({'pubsub#deliver': '1'}, request.options.getValues())
    809872
    810873
    811874    def test_fromElementOptionsSetCancel(self):
     
    825888        """
    826889
    827890        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
    828         self.assertEqual({}, request.options)
     891        self.assertEqual('cancel', request.options.formType)
    829892
    830893
    831894    def test_fromElementOptionsSetBadFormType(self):
     
    10531116        self.assertEqual(JID('pubsub.example.org'), request.recipient)
    10541117        self.assertEqual('test', request.nodeIdentifier)
    10551118        self.assertEqual({'pubsub#deliver_payloads': '0',
    1056                           'pubsub#persist_items': '1'}, request.options)
     1119                          'pubsub#persist_items': '1'},
     1120                         request.options.getValues())
    10571121
    10581122
    10591123    def test_fromElementConfigureSetCancel(self):
     
    10731137        """
    10741138
    10751139        request = pubsub.PubSubRequest.fromElement(parseXml(xml))
    1076         self.assertEqual({}, request.options)
     1140        self.assertEqual('cancel', request.options.formType)
    10771141
    10781142
    10791143    def test_fromElementConfigureSetBadFormType(self):
     
    18951959            return defer.succeed({'pubsub#deliver_payloads': '0',
    18961960                                  'pubsub#persist_items': '1',
    18971961                                  'pubsub#owner': JID('user@example.org'),
    1898                                   'x-myfield': ['a', 'b']})
     1962                                  'x-myfield': 'a'})
    18991963
    19001964        def cb(element):
    19011965            self.assertEqual('pubsub', element.name)
     
    19682032
    19692033        def configureSet(request):
    19702034            self.assertEqual({'pubsub#deliver_payloads': False,
    1971                               'pubsub#persist_items': True}, request.options)
     2035                              'pubsub#persist_items': True},
     2036                             request.options.getValues())
    19722037            return defer.succeed(None)
    19732038
    19742039        self.resource.getConfigurationOptions = getConfigurationOptions
     
    20402105
    20412106        def configureSet(request):
    20422107            self.assertEquals(['pubsub#deliver_payloads'],
    2043                               request.options.keys())
     2108                              request.options.fields.keys())
    20442109
    20452110        self.resource.getConfigurationOptions = getConfigurationOptions
    20462111        self.resource.configureSet = configureSet
     
    25952660        return d
    25962661
    25972662
     2663    def test_setConfigurationOptionsDict(self):
     2664        """
     2665        Options should be passed as a dictionary, not a form.
     2666        """
     2667
     2668        xml = """
     2669        <iq type='set' to='pubsub.example.org'
     2670                       from='user@example.org'>
     2671          <pubsub xmlns='http://jabber.org/protocol/pubsub#owner'>
     2672            <configure node='test'>
     2673              <x xmlns='jabber:x:data' type='submit'>
     2674                <field var='FORM_TYPE' type='hidden'>
     2675                  <value>http://jabber.org/protocol/pubsub#node_config</value>
     2676                </field>
     2677                <field var='pubsub#deliver_payloads'><value>0</value></field>
     2678                <field var='pubsub#persist_items'><value>1</value></field>
     2679              </x>
     2680            </configure>
     2681          </pubsub>
     2682        </iq>
     2683        """
     2684
     2685        def getConfigurationOptions():
     2686            return {
     2687                "pubsub#persist_items":
     2688                    {"type": "boolean",
     2689                     "label": "Persist items to storage"},
     2690                "pubsub#deliver_payloads":
     2691                    {"type": "boolean",
     2692                     "label": "Deliver payloads with event notifications"}
     2693                }
     2694
     2695        def setConfiguration(requestor, service, nodeIdentifier, options):
     2696            self.assertEquals({'pubsub#deliver_payloads': False,
     2697                               'pubsub#persist_items': True}, options)
     2698
     2699
     2700        self.service.getConfigurationOptions = getConfigurationOptions
     2701        self.service.setConfiguration = setConfiguration
     2702        return self.handleRequest(xml)
     2703
     2704
    25982705    def test_items(self):
    25992706        """
    26002707        Non-overridden L{PubSubService.items} yields unsupported error.
Note: See TracBrowser for help on using the repository browser.