From 48f0843ace5a8d383abe493b999e7bd4699598d7 Mon Sep 17 00:00:00 2001 From: Lance Stout Date: Wed, 14 Jul 2010 11:59:58 -0400 Subject: [PATCH] Added initial stanza object version of the xep_0004 plugin. Items/reported elements still need to be unit tested --- sleekxmpp/plugins/alt_0004.py | 330 ++++++++++++++++++++++++++++++++++ tests/test_forms.py | 90 ++++++++++ 2 files changed, 420 insertions(+) create mode 100644 sleekxmpp/plugins/alt_0004.py create mode 100644 tests/test_forms.py diff --git a/sleekxmpp/plugins/alt_0004.py b/sleekxmpp/plugins/alt_0004.py new file mode 100644 index 0000000..b38a491 --- /dev/null +++ b/sleekxmpp/plugins/alt_0004.py @@ -0,0 +1,330 @@ +""" + SleekXMPP: The Sleek XMPP Library + Copyright (C) 2010 Nathanael C. Fritz, Lance J.T. Stout + This file is part of SleekXMPP. + + See the file license.txt for copying permission. +""" + +import logging +import copy +from . import base +from .. xmlstream.handler.callback import Callback +from .. xmlstream.matcher.xpath import MatchXPath +from .. xmlstream.stanzabase import ElementBase, ET, JID +from .. stanza.message import Message + + +class Form(ElementBase): + namespace = 'jabber:x:data' + name = 'x' + plugin_attrib = 'form' + interfaces = set(('fields', 'instructions', 'items', 'reported', 'title', 'type', 'values')) + sub_interfaces = set(('title',)) + form_types = set(('cancel', 'form', 'result', 'submit')) + + def addField(self, var, ftype='text-single', label='', desc='', required=False, value=None, options=None): + field = FormField(parent=self) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + field['required'] = required + field['value'] = value + if options is not None: + field['options'] = options + return field + + def addItem(self, values): + itemXML = ET.Element('{%s}item' % self.namespace) + self.xml.append(itemXML) + reported_vars = self['reported'].keys() + for var in reported_vars: + fieldXML = ET.Element('{%s}field' % FormField.namespace) + itemXML.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['value'] = values.get(var, None) + + def addReported(self, var, ftype='text-single', label='', desc=''): + reported = self.xml.find('{%s}reported' % self.namespace) + if reported is None: + reported = ET.Element('{%s}reported' % self.namespace) + self.xml.append(reported) + fieldXML = ET.Element('{%s}field' % FormField.namespace) + reported.append(fieldXML) + field = FormField(xml=fieldXML) + field['var'] = var + field['type'] = ftype + field['label'] = label + field['desc'] = desc + return field + + def cancel(self): + self['type'] = 'cancel' + + def delFields(self): + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + self.xml.remove(fieldXML) + + def delInstructions(self): + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + self.xml.remove(instXML) + + def delItems(self): + itemsXML = self.xml.find('{%s}item' % self.namespace) + for itemXML in itemsXML: + self.xml.remove(itemXML) + + def delReported(self): + reportedXML = self.xml.find('{%s}reported' % self.namespace) + if reportedXML is not None: + self.xml.remove(reportedXML) + + def getFields(self): + fields = {} + fieldsXML = self.xml.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getInstructions(self): + instructions = '' + instsXML = self.xml.findall('{%s}instructions') + for instXML in instsXML: + instructions += instXML.text + + def getItems(self): + items = [] + itemsXML = self.xml.findall('{%s}item' % self.namespace) + for itemXML in itemsXML: + item = {} + fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + item[field['var']] = field['value'] + items.append(item) + return items + + def getReported(self): + fields = {} + fieldsXML = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, + FormField.namespace)) + for fieldXML in fieldsXML: + field = FormField(xml=fieldXML) + fields[field['var']] = field + return fields + + def getValues(self): + values = {} + fields = self.getFields() + for var in fields: + values[var] = fields[var]['value'] + return values + + def reply(self): + if self['type'] == 'form': + self['type'] = 'submit' + elif self['type'] == 'submit': + self['type'] = 'result' + + def setFields(self, fields): + del self['fields'] + for var in fields: + field = fields[var] + + # Remap 'type' to 'ftype' to match the addField method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addField(var, **field) + + def setInstructions(self, instructions): + instructions = instructions.split('\n') + for instruction in instructions: + inst = ET.Element('{%s}instructions' % self.namespace) + inst.text = instruction + self.xml.append(inst) + + def setItems(self, items): + for item in items: + self.addItem(item) + + def setReported(self, reported): + for var in reported: + field = reported[var] + + # Remap 'type' to 'ftype' to match the addReported method + ftype = field.get('type', 'text-single') + field['type'] = ftype + del field['type'] + field['ftype'] = ftype + + self.addReported(var, **field) + + def setValues(self, values): + fields = self.getFields() + for field in values: + fields[field]['value'] = values[field] + + +class FormField(ElementBase): + namespace = 'jabber:x:data' + name = 'field' + plugin_attrib = 'field' + interfaces = set(('answer', 'desc', 'required', 'value', 'options', 'label', 'type', 'var')) + sub_interfaces = set(('desc',)) + field_types = set(('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', + 'list-single', 'text-multi', 'text-private', 'text-single')) + multi_value_types = set(('hidden', 'jid-multi', 'list-multi', 'text-multi')) + multi_line_types = set(('hidden', 'text-multi')) + option_types = set(('list-multi', 'list-single')) + true_values = set((True, '1', 'true')) + + def addOption(self, label='', value=''): + if self['type'] in self.option_types: + opt = FieldOption(parent=self) + opt['label'] = label + opt['value'] = value + else: + raise ValueError("Cannot add options to a %s field." % self['type']) + + def delOptions(self): + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + self.xml.remove(optXML) + + def delRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + if reqXML is not None: + self.xml.remove(reqXML) + + def delValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + for valXML in valsXML: + self.xml.remove(valXML) + + def getAnswer(self): + return self.getValue() + + def getOptions(self): + options = [] + optsXML = self.xml.findall('{%s}option' % self.namespace) + for optXML in optsXML: + opt = FieldOption(xml=optXML) + options.append({'label': opt['label'], 'value':opt['value']}) + return options + + def getRequired(self): + reqXML = self.xml.find('{%s}required' % self.namespace) + return reqXML is not None + + def getValue(self): + valsXML = self.xml.findall('{%s}value' % self.namespace) + if len(valsXML) == 0: + return None + elif self['type'] == 'boolean': + return valsXML[0].text in self.true_values + elif self['type'] in self.multi_value_types: + values = [] + for valXML in valsXML: + if valXML.text is None: + valXML.text = '' + values.append(valXML.text) + if self['type'] == 'text-multi': + values = "\n".join(values) + return values + else: + return valsXML[0].text + + def setAnswer(self, answer): + self.setValue(answer) + + def setFalse(self): + self.setValue(False) + + def setOptions(self, options): + for value in options: + if isinstance(value, dict): + self.addOption(**value) + else: + self.addOption(value=value) + + def setRequired(self, required): + exists = self.getRequired() + if not exists and required: + self.xml.append(ET.Element('{%s}required' % self.namespace)) + elif exists and not required: + self.delRequired() + + def setTrue(self): + self.setValue(True) + + def setValue(self, value): + self.delValue() + valXMLName = '{%s}value' % self.namespace + + if self['type'] == 'boolean': + if value in self.true_values: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + else: + valXML = ET.Element(valXMLName) + valXML.text = 'true' + self.xml.append(valXML) + if self['type'] in self.multi_value_types: + if self['type'] in self.multi_line_types and isinstance(value, str): + value = value.split('\n') + if not isinstance(value, list): + value = [value] + for val in value: + valXML = ET.Element(valXMLName) + valXML.text = val + self.xml.append(valXML) + else: + if isinstance(value, list): + raise ValueError("Cannot add multiple values to a %s field." % self['type']) + valXML = ET.Element(valXMLName) + valXML.text = value + self.xml.append(valXML) + + +class FieldOption(ElementBase): + namespace = 'jabber:x:data' + name = 'option' + plugin_attrib = 'option' + interfaces = set(('label', 'value')) + sub_interfaces = set(('value',)) + + +class alt_0004(base.base_plugin): + """ + XEP-0004: Data Forms + """ + + def plugin_init(self): + self.xep = '0004' + self.description = 'Data Forms' + + self.xmpp.registerHandler( + Callback('Data Form', + MatchXPath('{%s}message/{%s}x' % (self.xmpp.default_ns, + Form.namespace)), + self.handle_form)) + + self.xmpp.stanzaPlugin(FormField, FieldOption) + self.xmpp.stanzaPlugin(Form, FormField) + self.xmpp.stanzaPlugin(Message, Form) + + def post_init(self): + base.base_plugin.post_init(self) + self.xmpp.plugin['xep_0030'].add_feature('jabber:x:data') + + def handle_form(self, message): + self.xmpp.event("message_xform", message) diff --git a/tests/test_forms.py b/tests/test_forms.py new file mode 100644 index 0000000..981d887 --- /dev/null +++ b/tests/test_forms.py @@ -0,0 +1,90 @@ +from sleektest import * +import sleekxmpp.plugins.alt_0004 as xep_0004 + + +class TestDataForms(SleekTest): + + def setUp(self): + self.stanzaPlugin(Message, xep_0004.Form) + self.stanzaPlugin(xep_0004.Form, xep_0004.FormField) + self.stanzaPlugin(xep_0004.FormField, xep_0004.FieldOption) + + def testMultipleInstructions(self): + """Testing using multiple instructions elements in a data form.""" + msg = self.Message() + msg['form']['instructions'] = "Instructions\nSecond batch" + + self.checkMessage(msg, """ + + + Instructions + Second batch + + + """, use_values=False) + + def testAddField(self): + """Testing adding fields to a data form.""" + + msg = self.Message() + form = msg['form'] + form.addField('f1', + ftype='text-single', + label='Text', + desc='A text field', + required=True, + value='Some text!') + + self.checkMessage(msg, """ + + + + A text field + + Some text! + + + + """, use_values=False) + + form['fields'] = {'f1': {'type': 'text-single', + 'label': 'Username', + 'required': True}, + 'f2': {'type': 'text-private', + 'label': 'Password', + 'required': True}, + 'f3': {'type': 'text-multi', + 'label': 'Message', + 'value': 'Enter message.\nA long one even.'}, + 'f4': {'type': 'list-single', + 'label': 'Message Type', + 'options': [{'label': 'Cool!', + 'value': 'cool'}, + {'label': 'Urgh!', + 'value': 'urgh'}]}} + self.checkMessage(msg, """ + + + + + + + + + + Enter message. + A long one even. + + + + + + + + """, use_values=False) + +suite = unittest.TestLoader().loadTestsFromTestCase(TestDataForms)