diff --git a/src/common/commands.py b/src/common/commands.py index 47907f009..da0aea905 100644 --- a/src/common/commands.py +++ b/src/common/commands.py @@ -53,8 +53,8 @@ class AdHocCommand: cmd.addChild('actions', attrs, actions) return response, cmd - def badRequest(self): - self.connection.connection.send(xmpp.Error(xmpp.NS_STANZAS+' bad-request')) + def badRequest(self, stanza): + self.connection.connection.send(xmpp.Error(stanza, xmpp.NS_STANZAS+' bad-request')) def cancel(self, request): response, cmd = self.buildResponse(request, status='canceled') @@ -68,17 +68,20 @@ class ChangeStatusCommand(AdHocCommand): @staticmethod def isVisibleFor(samejid): ''' Change status is visible only if the entity has the same bare jid. ''' + print 'isVisibleFor', samejid + return True # TODO: Remove that! return samejid def execute(self, request): # first query... response, cmd = self.buildResponse(request, defaultaction='execute', actions=['execute']) - cmd.addChild(node=dataforms.DataForm( + cmd.addChild(node=dataforms.SimpleDataForm( title='Change status', instructions='Set the presence type and description', fields=[ - dataforms.DataField('list-single', 'presence-type', + dataforms.Field('list-single', + var='presence-type', label='Type of presence:', options=[ (u'free-for-chat', u'Free for chat'), @@ -89,7 +92,8 @@ class ChangeStatusCommand(AdHocCommand): (u'offline', u'Offline - disconnect')], value='online', required=True), - dataforms.DataField('text-multi', 'presence-desc', + dataforms.Field('text-multi', + var='presence-desc', label='Presence description:')])) self.connection.connection.send(response) @@ -102,23 +106,25 @@ class ChangeStatusCommand(AdHocCommand): def changestatus(self, request): # check if the data is correct try: - form=dataforms.DataForm(node=request.getTag('command').getTag('x')) - except TypeError: - self.badRequest() + form=dataforms.SimpleDataForm(extend=request.getTag('command').getTag('x')) + except: + self.badRequest(request) return False try: - presencetype = form['presence-type'] - if not presencetype in ('free-for-chat', 'online', 'away', 'xa', 'dnd', 'offline'): - self.badRequest() + presencetype = form['presence-type'].value + if not presencetype in \ + ('free-for-chat', 'online', 'away', 'xa', 'dnd', 'offline'): + self.badRequest(request) return False - except KeyError: - self.badRequest() + except: # KeyError if there's no presence-type field in form or + # AttributeError if that field is of wrong type + self.badRequest(request) return False try: - presencedesc = form['presence-desc'] - except KeyError: + presencedesc = form['presence-desc'].value + except: # same exceptions as in last comment presencedesc = u'' response, cmd = self.buildResponse(request, status='completed') @@ -150,6 +156,7 @@ class ConnectionCommands: return xmpp.JID(jid).getStripped() == self.getOurBareJID() def commandListQuery(self, con, iq_obj): + print 'commandListQuery' iq = iq_obj.buildReply('result') jid = helpers.get_full_jid_from_iq(iq_obj) q = iq.getTag('query') @@ -224,11 +231,12 @@ class ConnectionCommands: obj = self.__sessions[magictuple] try: - if action == 'cancel': rc = obj.cancel(iq_obj) - elif action == 'prev': rc = obj.prev(iq_obj) - elif action == 'next': rc = obj.next(iq_obj) - elif action == 'execute': rc = obj.execute(iq_obj) - elif action == 'complete': rc = obj.complete(iq_obj) + if action == 'cancel': rc = obj.cancel(iq_obj) + elif action == 'prev': rc = obj.prev(iq_obj) + elif action == 'next': rc = obj.next(iq_obj) + elif action == 'execute' or action is None: + rc = obj.execute(iq_obj) + elif action == 'complete': rc = obj.complete(iq_obj) else: # action is wrong. stop the session, send error raise AttributeError diff --git a/src/common/dataforms-new.py b/src/common/dataforms-new.py deleted file mode 100644 index 032aed4d3..000000000 --- a/src/common/dataforms-new.py +++ /dev/null @@ -1,387 +0,0 @@ -# this will go to src/common/xmpp later, for now it is in src/common -""" This module contains wrappers for different parts of data forms (JEP 0004). For information -how to use them, read documentation. """ - -import xmpp - -# exceptions used in this module -class Error(Exception): pass # base class -class UnknownDataForm(Error): pass # when we get xmpp.Node which we do not understand -class WrongFieldValue(Error): pass # when we get xmpp.Node which contains bad fields - -# helper class to change class of already existing object -class ExtendedNode(xmpp.Node, object): - def __new__(cls, *a, **b): - print 'Extends.__new__' - if 'extend' not in b.keys(): - return object.__new__(cls) - - extend = b['extend'] - assert issubclass(cls, extend.__class__) - extend.__class__ = cls - return extend - -# helper decorator to create properties in cleaner way -def nested_property(f): - ret = f() - p = {'doc': f.__doc__} - for v in ('fget', 'fset', 'fdel', 'doc'): - if v in ret.keys(): p[v]=ret[v] - return property(**p) - -# helper to create fields from scratch -def Field(typ, **attrs): - f = { - 'boolean': BooleanField, - 'fixed': StringField, - 'hidden': StringField, - 'text-private': StringField, - 'text-single': StringField, - 'jid-multi': ListField, - 'jid-single': ListField, - 'list-multi': ListField, - 'list-single': ListField, - 'text-multi': TextMultiField, - }[typ] - for key, value in attrs.iteritems(): - f.setattr(key, value) - return f - -class DataField(ExtendedNode): - """ Keeps data about one field - var, field type, labels, instructions... """ - def __init__(self, typ=None, var=None, value=None, label=None, desc=None, required=None, - options=None, extend=None): - - if extend is None: - super(DataField, self).__init__(self, 'field') - - self.type = typ - self.var = var - self.value = value - self.label = label - self.desc = desc - self.required = required - self.options = options - - @nested_property - def type(): - '''Type of field. Recognized values are: 'boolean', 'fixed', 'hidden', 'jid-multi', - 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', - 'text-single'. If you set this to something different, DataField will store - given name, but treat all data as text-single.''' - def fget(self): - t = self.getAttr('type') - if t is None: return 'text-single' - return t - def fset(self, value): - assert isinstance(value, basestring) - self.setAttr('type', value) - return locals() - - @nested_property - def var(): - '''Field identifier.''' - def fget(self): - return self.getAttr('var') - def fset(self, value): - assert isinstance(value, basestring) - self.setAttr('var', value) - def fdel(self): - self.delAttr('var') - return locals() - - @nested_property - def label(): - '''Human-readable field name.''' - def fget(self): - return self.getAttr('label') - def fset(self, value): - assert isinstance(value, basestring) - self.setAttr('label', value) - def fdel(self): - self.delAttr('label') - return locals() - - @nested_property - def description(): - '''Human-readable description of field meaning.''' - def fget(self): - return self.getTagData('desc') or u'' - def fset(self, value): - assert isinstance(value, basestring) - if value == '': - fdel(self) - else: - self.setTagData('desc', value) - def fdel(self): - t = self.getTag('desc') - if t is not None: - self.delChild(t) - return locals() - - @nested_property - def required(): - '''Controls whether this field required to fill. Boolean.''' - def fget(self): - return boolean(self.getTag('required')) - def fset(self, value): - t = self.getTag('required') - if t and not value: - self.delChild(t) - elif not t and value: - self.addChild('required') - return locals() - -class BooleanField(DataField): - @nested_property - def value(): - '''Value of field. May contain True, False or None.''' - def fget(self): - v = self.getTagData('value') - if v in ('0', 'false'): return False - if v in ('1', 'true'): return True - if v is None: return None - raise WrongFieldValue - def fset(self, value): - self.setTagData('value', value and '1' or '0') - def fdel(self, value): - t = self.getTag('value') - if t is not None: - self.delChild(t) - return locals() - -class StringField(DataField): - ''' Covers fields of types: fixed, hidden, text-private, text-single. ''' - @nested_property - def value(): - '''Value of field. May be any unicode string.''' - def fget(self): - return self.getTagData('value') or u'' - def fset(self, value): - assert isinstance(value, basestring) - if value == '': - return fdel(self) - self.setTagData('value', value) - def fdel(self): - t = self.getTag('value') - if t is not None: - self.delChild(t) - return locals() - -class ListField(DataField): - ''' Covers fields of types: jid-multi, jid-single, list-multi, list-single. ''' - @nested_property - def values(): - '''Values held in field.''' - def fget(self): - values = [] - for element in iter_elements(self, 'value'): - values.append(element.getData()) - return values - def fset(self, values): - fdel(self) - for value in values: - self.addChild('value').setData(value) - def fdel(self): - for element in iter_elements(self, 'value'): - self.delChild(element) - return locals() - - def iter_values(): - for element in iter_elements(self, 'value'): - yield element.getData() - - @nested_property - def options(): - '''Options.''' - def fget(self): - options = [] - for element in iter_elements(self, 'option'): - v = element.getTagData('value') - if v is None: raise WrongFieldValue - options.append((element.getAttr('label'), v)) - return options - def fset(self, values): - fdel(self) - for label, value in values: - self.addChild('option', {'label': label}).setTagData('value', value) - def fdel(self): - for element in iter_elements(self, 'option'): - self.delChild(element) - return locals() - - def iter_options(self): - for element in iter_elements(self, 'option'): - v = element.getTagData('value') - if v is None: raise WrongFieldValue - yield (element.getAttr('label'), v) - -class TextMultiField(DataField): - @nested_property - def value(): - '''Value held in field.''' - def fget(self): - value = u'' - for element in iter_elements(self, 'value'): - value += '\n' + element.getData() - return value[1:] - def fset(self, value): - fdel(self) - if value == '': return - for line in value.split('\n'): - self.addChild('value').setData(line) - def fdel(self): - for element in iter_elements(self, 'value'): - self.delChild(element) - return locals() - -class DataRecord(ExtendedNode): - '''The container for data fields - an xml element which has DataField - elements as children.''' - def __init__(self, fields=None, associated=None, extend=None): - self.associated = associated - self.vars = {} - if extend is None: - # we have to build this object from scratch - xmpp.Node.__init__(self) - - if fields is not None: self.fields = fields - else: - # we already have xmpp.Node inside - try to convert all - # fields into DataField objects - for field in self.iterTags('field'): - if not isinstance(field, DataField): - DataField(extend=field) - self.vars[field.var] = field - - @nested_property - def fields(): - '''List of fields in this record.''' - def fget(self): - return self.getTags('field') - def fset(self, fields): - fdel(self) - for field in fields: - if not isinstance(field, DataField): - DataField(extend=field) - self.addChild(node=field) - def fdel(self): - for element in self.iterTags('field'): - self.delChild(element) - return locals() - - def iter_fields(self): - ''' Iterate over fields in this record. Do not take associated - into account. ''' - for field in self.iterTags('field'): - yield field - - def iter_with_associated(self): - ''' Iterate over associated, yielding both our field and - associated one together. ''' - for field in self.associated.iter_fields(): - yield self[field.var], field - - def __getitem__(self, item): - return self.vars[item] - -class DataForm(ExtendedNode): - def __init__(self, replyto=None, extend=None): - if extend is None: - # we have to build form from scratch - xmpp.Node.__init__(self, 'x', attrs={'xmlns': xmpp.NS_DATA}) - - @nested_property - def type(): - ''' Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'. - 'form' - this form is to be filled in; you can do: - filledform = DataForm(replyto=thisform)...''' - def fget(self): - return self.getAttr('type') - def fset(self): - assert type in ('form', 'submit', 'cancel', 'result') - self.setAttr('type', type) - return locals() - - @nested_property - def title(): - ''' Title of the form. Human-readable, should not contain any \\r\\n.''' - def fget(self): - return self.getTagData('title') - def fset(self): - self.setTagData('title', title) - def fdel(self): - try: - self.delChild('title') - except ValueError: - pass - return locals() - - @nested_property - def instructions(): - ''' Instructions for this form. Human-readable, may contain \\r\\n. ''' - # TODO: the same code is in TextMultiField. join them - def fget(self): - value = u'' - for value in self.iterTags('value'): - value += '\n' + value.getData() - return value[1:] - def fset(self, value): - fdel(self) - if value == '': return - for line in value.split('\n'): - self.addChild('value').setData(line) - def fdel(self): - for value in self.iterTags('value'): - self.delChild(value) - return locals() - -class SimpleDataForm(DataForm, DataRecord): - def __init__(self, extend=None): - if extend is None: - # we have to build form from scratch - DataForm.__init__(self) - DataRecord.__init__(self, extend=self, associated=self) - else: - # we already have node, just change it to real form object - pass - -class MultipleDataForm(DataForm): - def __init__(self): - # all records, recorded into DataRecords - pass - - @nested_property - def items(): - ''' A list of all records. ''' - def fget(self): - return list(self.iter_records()) - def fset(self, records): - fdel(self) - for record in records: - if not isinstance(record, DataRecord): - DataRecord(extend=record) - self.addChild(node=record) - def fdel(self): - for record in self.iterTags('record'): - self.delChild(record) - return locals() - - def iter_records(): - for record in self.iterTags('item'): - yield item - - @nested_property - def recorded(): - ''' DataRecord that contains descriptions of fields in records.''' - def fget(self): - return self.getTag('recorded') - def fset(self, record): - try: - self.delChild('recorded') - except: - pass - - record.setName('recorded') - self.addChild(node=record) - return locals() diff --git a/src/common/dataforms.py b/src/common/dataforms.py index f844d5dbf..fc2ab6c04 100644 --- a/src/common/dataforms.py +++ b/src/common/dataforms.py @@ -1,560 +1,415 @@ # this will go to src/common/xmpp later, for now it is in src/common +""" This module contains wrappers for different parts of data forms (JEP 0004). For information +how to use them, read documentation. """ import xmpp -# Helpers, to handle multi-element values like field values of instructions elements +# exceptions used in this module +class Error(Exception): pass # base class +class UnknownDataForm(Error): pass # when we get xmpp.Node which we do not understand +class WrongFieldValue(Error): pass # when we get xmpp.Node which contains bad fields -def get_multiple_tag_value(node, childname): - """ Join by u'/n' all occurences of childname in node, as described in JEP-0004 for value data for - *-multi fields.""" - assert isinstance(node, xmpp.Node) - assert isinstance(childname, basestring) +# helper class to change class of already existing object +class ExtendedNode(xmpp.Node, object): + @classmethod + def __new__(cls, *a, **b): + if 'extend' not in b.keys(): + return object.__new__(cls) - nodes = node.getTags(childname) - values = (node.getData().decode('utf-8') for node in nodes) # using python2.4 iterators - return u"\n".join(values) + extend = b['extend'] + assert issubclass(cls, extend.__class__) + extend.__class__ = cls + return extend -def set_multiple_tag_value(node, childname, value): - """ Set children nodes to value, splitting lines between different nodes, as described in JEP-0004 - for value data for *-multi fields. If you try to set the value to None, all tag values will be - deleted. """ - assert isinstance(node, xmpp.Node) - assert isinstance(childname, basestring) - assert isinstance(value, basestring) or value is None +# helper decorator to create properties in cleaner way +def nested_property(f): + ret = f() + p = {'doc': f.__doc__} + for v in ('fget', 'fset', 'fdel', 'doc'): + if v in ret.keys(): p[v]=ret[v] + return property(**p) - del_multiple_tag_value(node, childname) +# helper to create fields from scratch +def Field(typ, **attrs): + ''' Helper function to create a field of given type. ''' + f = { + 'boolean': BooleanField, + 'fixed': StringField, + 'hidden': StringField, + 'text-private': StringField, + 'text-single': StringField, + 'jid-multi': ListMultiField, + 'jid-single': ListSingleField, + 'list-multi': ListMultiField, + 'list-single': ListSingleField, + 'text-multi': TextMultiField, + }[typ](typ=typ, **attrs) + return f - if value is None: return +def ExtendField(node): + ''' Helper function to extend a node to field of appropriate type. ''' + # TODO: move the dict out + typ=node.getAttr('type') + f = { + 'boolean': BooleanField, + 'fixed': StringField, + 'hidden': StringField, + 'text-private': StringField, + 'text-single': StringField, + 'jid-multi': ListMultiField, + 'jid-single': ListSingleField, + 'list-multi': ListMultiField, + 'list-single': ListSingleField, + 'text-multi': TextMultiField, + }[typ](extend=node) + return f - values = value.split(u'\n') - nodes = (xmpp.Node(childname, payload=[value.encode('utf-8')]) for value in values)# using python2.4 iterators - for new in nodes: - node.addChild(node=new) +class DataField(ExtendedNode): + """ Keeps data about one field - var, field type, labels, instructions... """ + def __init__(self, typ=None, var=None, value=None, label=None, desc=None, required=False, + options=None, extend=None): -def del_multiple_tag_value(node, childname): - """ Delete a value from node, when the value is splitted between many childname elements inside - node, as described in JEP-0004 for value for *-multi fields. It also removes every instance of - childname in node. - """ + if extend is None: + ExtendedNode.__init__(self, 'field') - try: - while node.delChild(childname): - pass - except ValueError: - pass + self.type = typ + self.var = var + if value is not None: self.value = value + if label is not None: self.label = label + if desc is not None: self.desc = desc + self.required = required + self.options = options -def iter_elements(node, childname): - """ Iterate over childname children of node element. """ - for element in node.getChildren(): - if isinstance(element, xmpp.Node) and element.getName()==childname: - yield element - -(DATAFORM_SINGLE, DATAFORM_MULTIPLE) = range(2) - -class RecordDoesNotContainRequiredField(Exception): pass -class BadDataFormNode(Exception): pass - -class DataForm(xmpp.Node, object): - """ Data form as described in JEP-0004. """ - # NOTES: single forms contain children nodes 'field' of type DataField, - # NOTES: multiple forms contain children do not contain 'item' elements of type DataRecord - # NOTES: - these are created only when needed - - def __init__(self, typ=None, title=None, instructions=None, fields=None, records=None, node=None, tofill=None): - """ Create new node, based on the node given by 'node' parameter, or new. - You can prefill some form properties, look at their description for meaning. - - You can also set 'tofill' to DataForm to get a form of type 'submit' with the - same fields as in the form given, but without unnecessary data like - instructions or title. Also the type will be set to 'submit' by default.""" - # assert that at most one of these fields (node and tofill) is filled - assert node is None or isinstance(node, xmpp.Node) - assert tofill is None or isinstance(tofill, xmpp.Node) - assert node is None or tofill is None - - assert typ in (None, 'form', 'submit', 'cancel', 'result') - assert title is None or isinstance(title, basestring) - assert instructions is None or isinstance(instructions, basestring) - assert (fields is None or fields.__iter__) - - xmpp.Node.__init__(self, 'x', node=node) - self.setNamespace(xmpp.NS_DATA) - - if tofill is not None: - self._mode = tofill.mode - self.fields = (field for field in tofill.fields if field.type!='fixed') - self.records = tofill.records - self.type = 'submit' - for field in self.iter_fields(): - field.required=False - del field.label - del field.options - del field.description - - elif node is not None: - # helper function - convert all elements to DataField - def convert_fields(node): - toadd=[] - for field in iter_elements(node, 'field'): - if not isinstance(field, DataField): - field=DataField(node=field) - toadd.append(field) - del_multiple_tag_value(node, 'field') - for field in toadd: - node.addChild(node=field) - - # if there is element, the form contains multiple records - if self.getTag('recorded') is not None: - # in multiple-record forms there must not be any fields not in - if self.getTag('field') is not None: raise BadDataNodeForm - self._mode = DATAFORM_MULTIPLE - - # change every in every element to DataField object - for item in iter_elements(self, 'item'): - convert_fields(item) - - # the same in element - convert_fields(self.getTag('recorded')) - else: - self._mode = DATAFORM_SINGLE - - convert_fields(self) - - else: # both tofill and node are None - if typ is None: typ='result' - if records is not None and len(records)>1: - self._mode = DATAFORM_MULTIPLE - else: - self._mode = DATAFORM_SINGLE - - if typ is not None: self.type = typ - if title is not None: self.title = title - if instructions is not None: self.instructions = instructions - if fields is not None: self.fields = fields - if records is not None: self.records = records - - def get_type(self): - return self.getAttr('type') - - def set_type(self, type): - assert type in ('form', 'submit', 'cancel', 'result') - self.setAttr('type', type) - - type = property(get_type, set_type, None, - """Form type, one of: - 'form', when it is meant to complete, - 'submit', when it is meant to transport completed form, - 'cancel', when it is meant to cancel the process, - 'result', when it is meant to provide some data. (the default)""") - - def get_title(self): - return self.getTagData('title') - - def set_title(self, title): - self.setTagData('title', title) - - def del_title(self): - try: - self.delChild('title') - except ValueError: - pass - - title = property(get_title, set_title, del_title, - "Form title, in unicode, from element.") - - def get_instructions(self): - return get_multiple_tag_value(self, 'instructions') - - def set_instructions(self, data): - assert isinstance(data, basestring) - set_multiple_tag_value(self, 'instructions', data) - - def del_instructions(self): - del_multiple_tag_value(self, 'instructions') - - instructions = property(get_instructions, set_instructions, None, - "Instructions how to fill the form, in unicode, from <instructions/> element.") - - def get_mode(self): - return self._mode - - def set_mode(self, mode): - assert mode in (DATAFORM_SINGLE, DATAFORM_MULTIPLE) - assert self.getTag('field') is None - assert self.getTag('reported') is None - assert self.getTag('item') is None - self._mode = mode - - mode = property(get_mode, set_mode, None, - """Data form mode: DATAFORM_SINGLE or DATAFORM_MULTIPLE, if the form contains - more than one record of data. Changing mode is allowed as long as there is no - values in form.""") - - def iter_records(self): - if self.mode is DATAFORM_SINGLE: - yield DataRecord(node=self, associated=self) - else: - for i in iter_elements(self, 'item'): - yield DataRecord(node=i, associated=self) - - def get_records(self): - return list(self.iter_records()) - - def set_records(self, records): - ''' Set new records. For single dataform only the first element in - 'records' iterable will be chosen. - - Elements from the iterable must be a dictionary (with keys equal to - field names and values filled with field value) or any xmpp.Node - containing <field/> elements with appropriate format according to JEP-0004. - Therefore, you can pass an array of single forms, all of them with the - same fields, to create one big multiple form.''' - if self.mode is DATAFORM_SINGLE: - # get only first record... - record = records.__iter__().next() - - if isinstance(record, dict): - for var, value in record: - self[var]=value - elif isinstance(record, xmpp.Node): - for field in iter_elements(record, 'field'): - self[field.getAttr('var')]=field.getAttr('value') - else: - assert 'This should not happen' and False - else: - self.del_records() - for record in records: - newitem = self.addChild('item') - if isinstance(record, dict): - for key, value in record.iteritems(): - newitem.addChild(node=DataField( - var=key, - value=value, - typ=self.get_field(key).type)) - elif isinstance(record, xmpp.Node): - for field in iter_elements(record, 'field'): - newitem.addChild(node=DataField( - var=field.getAttr('var'), - value=field.getAttr('value'), - typ=field.getAttr('type'))) - else: - assert 'This should not happen' and False - - def del_records(self): - if self.mode is DATAFORM_SINGLE: - # removing values from every field - for field in self.iter_fields(): - del_multiple_tag_value(field, "value") - else: - # removing all <items/> elements - del_multiple_tag_value(self, "items") - - records = property(get_records, set_records, del_records, - """Records kept in this form; if in DATAFORM_SINGLE mode, there will be exactly - one record, otherwise there might be more or less records.""") - - def iter_fields(self): - if self.mode is DATAFORM_SINGLE: - container = self - else: - container = self.getTag("recorded") - - return iter_elements(container, 'field') - - def get_field(self, fieldvar): - ''' Find DataField that has fieldvar as var. ''' - for field in self.iter_fields(): - if field.var == fieldvar: return field - raise KeyError, "This form does not contain %r field." % fieldvar - - def get_fields(self): - return list(self.iter_fields) - - def set_fields(self, fields): - if self.mode is DATAFORM_SINGLE: - del_multiple_tag_value(self, "field") - for field in fields: - assert isinstance(field, DataField) - self.addChild(node=field) - else: - assert len(self.records)==0 - try: - self.delChild('recorded') - except ValueError: - pass - recorded = self.addChild('recorded') - for field in fields: - if not isinstance(field, DataField): - field = DataField(field) - recorded.addChild(node=field) - - def del_fields(self): - if self.mode is DATAFORM_SINGLE: - del_multiple_tag_value(self, "field") - else: - try: - self.delChild('recorded') - except ValueError: - pass - - fields = property(get_fields, set_fields, del_fields, - """Fields in this form; a list; if in DATAFORM_SINGLE mode, you should not - set their values directly.""") - - def __getitem__(self, var): - for field in self.iter_fields(): - if field.var==var: - return field.value - raise KeyError, "This form does not contain %r field." % var - - def __setitem__(self, var, value): - for field in self.iter_fields(): - if field.var==var: - field.value=value - return - raise KeyError, "This form does not contain %r field." % var - - def __contains__(self, name): - for field in self.iter_fields(): - if field.var==name: - return True - else: - return False - -class DataField(xmpp.Node, object): - def __init__(self, typ=None,var=None, value=None, label=None, desc=None, - required=None, options=None, node=None): - - assert typ in ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', - 'list-single', 'text-multi', 'text-private', 'text-single',None) - - xmpp.Node.__init__(self, 'field', node=node) - if typ is not None: self.type = typ - if var is not None: self.var = var - if label is not None: self.label = label - if desc is not None: self.description = desc - if required is not None: self.required = required - if value is not None: self.value = value - if options is not None: self.options = options - - def get_type(self): - # JEP says that if we don't understand field name, we should treat it as text-single - t = self.getAttr('type') - if t not in ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', - 'list-single', 'text-multi', 'text-private'): - return 'text-single' - else: + @nested_property + def type(): + '''Type of field. Recognized values are: 'boolean', 'fixed', 'hidden', 'jid-multi', + 'jid-single', 'list-multi', 'list-single', 'text-multi', 'text-private', + 'text-single'. If you set this to something different, DataField will store + given name, but treat all data as text-single.''' + def fget(self): + t = self.getAttr('type') + if t is None: return 'text-single' return t - - def set_type(self, typ): - assert typ in ('boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', 'list-multi', - 'list-single', 'text-multi', 'text-private', 'text-single') - - if typ!='text-single': - self.setAttr('type', typ) - else: - try: - self.delAttr('type') - except KeyError: - pass - - type = property(get_type, set_type, None, - """ Field type. One of: 'boolean', 'fixed', 'hidden', 'jid-multi', 'jid-single', - 'list-multi', 'list-single', 'text-multi', 'text-private', 'text-single'.""") - - def get_var(self): - return self.getAttr('var') - - def set_var(self, var): - self.setAttr('var', var) - - def del_var(self): - try: + def fset(self, value): + assert isinstance(value, basestring) + self.setAttr('type', value) + return locals() + + @nested_property + def var(): + '''Field identifier.''' + def fget(self): + return self.getAttr('var') + def fset(self, value): + assert isinstance(value, basestring) + self.setAttr('var', value) + def fdel(self): self.delAttr('var') - except KeyError: - pass + return locals() - var = property(get_var, set_var, del_var, - """ Field name. """) - - def get_label(self): - return self.getAttr('label') - - def set_label(self, label): - self.setAttr('label', label) - - def del_label(self): - try: + @nested_property + def label(): + '''Human-readable field name.''' + def fget(self): + return self.getAttr('label') + def fset(self, value): + assert isinstance(value, basestring) + self.setAttr('label', value) + def fdel(self): self.delAttr('label') - except KeyError: - pass + return locals() - label = property(get_label, set_label, del_label, - """ Human-readable name for field. """) + @nested_property + def description(): + '''Human-readable description of field meaning.''' + def fget(self): + return self.getTagData('desc') or u'' + def fset(self, value): + assert isinstance(value, basestring) + if value == '': + fdel(self) + else: + self.setTagData('desc', value) + def fdel(self): + t = self.getTag('desc') + if t is not None: + self.delChild(t) + return locals() - def get_description(self): - return self.getTagData('desc') - - def set_description(self, desc): - self.setTagData('desc', desc) - - def del_description(self): - try: - self.delChild('desc') - except ValueError: - pass - - description = property(get_description, set_description, del_description, - """ A natural-language description of the field. It should not contain - newlines. """) - - def get_required(self): - return self.getTag('required') is not None - - def set_required(self, req): - assert req in (True, False) - - if self.getTag('required') is not None: - if req is False: - self.delChild('required') - else: - if req is True: + @nested_property + def required(): + '''Controls whether this field required to fill. Boolean.''' + def fget(self): + return boolean(self.getTag('required')) + def fset(self, value): + t = self.getTag('required') + if t and not value: + self.delChild(t) + elif not t and value: self.addChild('required') + return locals() - required = property(get_required, set_required, None, - """ If this is set to True, the field is required for form to be valid. """) +class BooleanField(DataField): + @nested_property + def value(): + '''Value of field. May contain True, False or None.''' + def fget(self): + v = self.getTagData('value') + if v in ('0', 'false'): return False + if v in ('1', 'true'): return True + if v is None: return None + raise WrongFieldValue + def fset(self, value): + self.setTagData('value', value and '1' or '0') + def fdel(self, value): + t = self.getTag('value') + if t is not None: + self.delChild(t) + return locals() - def iter_values(self): - assert self.type in ('list-single', 'list-multi', 'jid-multi') - - for element in self.getChildren(): - if not isinstance(element, xmpp.Node): continue - if not element.getName()=='value': continue - yield element.getData().decode('utf-8') - - def get_value(self): - if self.type in ('boolean',): - if self.getTagData('value') in (1, 'true'): - return True - else: - return False - - elif self.type in ('fixed','text-multi'): - return get_multiple_tag_value(self, 'value') - - elif self.type in ('jid-multi', 'list-multi'): - return [value.getData() for value in self.getTags('value')] - - elif self.type in ('hidden', 'jid-single', 'list-single', 'text-single', 'text-private') or True: - return self.getTagData('value') - - def set_value(self, value): - if self.type in ('boolean',): - if value: - self.setTagData('value', '1') - else: - self.setTagData('value', '0') - - elif self.type in ('fixed','text-multi'): - set_multiple_tag_value(self, 'value', value) - - elif self.type in ('jid-multi', 'list-multi'): - del_multiple_tag_value(self, 'value') - for item in value: - self.addChild('value', {}, (item,)) - - elif self.type in ('hidden', 'jid-single', 'list-single', 'text-single', 'text-private'): +class StringField(DataField): + ''' Covers fields of types: fixed, hidden, text-private, text-single. ''' + @nested_property + def value(): + '''Value of field. May be any unicode string.''' + def fget(self): + return self.getTagData('value') or u'' + def fset(self, value): + assert isinstance(value, basestring) + if value == '': + return fdel(self) self.setTagData('value', value) + def fdel(self): + t = self.getTag('value') + if t is not None: + self.delChild(t) + return locals() - def del_value(self): - del_multiple_tag_value(self, 'value') - - value = property(get_value, set_value, del_value, - """ The value of field. Depending on the type, it is a boolean, a unicode string or a list - of stings. """) +class ListField(DataField): + ''' Covers fields of types: jid-multi, jid-single, list-multi, list-single. ''' + @nested_property + def options(): + '''Options.''' + def fget(self): + options = [] + for element in self.iterTags('option'): + v = element.getTagData('value') + if v is None: raise WrongFieldValue + options.append((element.getAttr('label'), v)) + return options + def fset(self, values): + fdel(self) + for value, label in values: + self.addChild('option', {'label': label}).setTagData('value', value) + def fdel(self): + for element in self.iterTags('option'): + self.delChild(element) + return locals() def iter_options(self): - """ Yields a pair: label, value """ - for element in self.getChildren(): - if not isinstance(element, xmpp.Node): continue - if not element.getName()=='option': continue - try: - yield element.getAttr('label'), element.getTag('value').getData() - except TypeError: - raise BadDataFormNode + for element in self.iterTags('option'): + v = element.getTagData('value') + if v is None: raise WrongFieldValue + yield (element.getAttr('label'), v) - def get_options(self): - """ Returns a list of tuples: (label, value). """ - return [(tag.getAttr('label'), tag.getTag('value').getData()) for tag in self.getTags('option')] +class ListSingleField(ListField, StringField): + '''Covers list-single and jid-single fields.''' + pass - def set_options(self, options): - """ Options need to be a list of tuples (label, value), both unicode. """ - assert options.__iter__ +class ListMultiField(ListField): + '''Covers list-multi and jid-multi fields.''' + @nested_property + def values(): + '''Values held in field.''' + def fget(self): + values = [] + for element in self.iterTags('value'): + values.append(element.getData()) + return values + def fset(self, values): + fdel(self) + for value in values: + self.addChild('value').setData(value) + def fdel(self): + for element in self.iterTags('value'): + self.delChild(element) + return locals() + + def iter_values(): + for element in self.iterTags('value'): + yield element.getData() - del_multiple_tag_value(self, 'option') - for option in options: - assert option[0] is None or isinstance(option[0], unicode) - assert isinstance(option[1], unicode) - if option[0] is None: - attr={} - else: - attr={'label': option[0].encode('utf-8')} - self.addChild('option', attr, (xmpp.Node('value', {}, (option[1].encode('utf-8'),)),)) +class TextMultiField(DataField): + @nested_property + def value(): + '''Value held in field.''' + def fget(self): + value = u'' + for element in self.getTags('value'): # TODO: iter! + value += '\n' + element.getData() + return value[1:] + def fset(self, value): + fdel(self) + if value == '': return + for line in value.split('\n'): + self.addChild('value').setData(line) + def fdel(self): + for element in self.iterTags('value'): + self.delChild(element) + return locals() - def del_options(self): - del_multiple_tag_value(self, 'option') - - options = property(get_options, set_options, del_options, - """ Options to choose between in list-* fields. """) - -class DataRecord(xmpp.Node): - """ Class to store fields. May be used as temporary storage (for example when reading a list of - fields from DataForm in DATAFORM_SINGLE mode), may be used as permanent storage place (for example - for DataForms in DATAFORM_MULTIPLE mode). It expects that every <field/> element is actually - a DataField instance. Read-only.""" - def __init__(self, fields=None, node=None, associated=None): - ''' Create new instance of DataRecord. You can use 'associated' to provide form which - contains field descriptions. This way iterating over fields in this record will be - in the same order as fields in given DataForm, with None when field does not exist - in this record. ''' - assert (fields is None) or (node is None) - assert (fields is None) or (fields.__iter__) - assert (node is None) or (isinstance(node, xmpp.Node)) - assert (associated is None) or (isinstance(associated, DataForm)) - - self.vars = {} +class DataRecord(ExtendedNode): + '''The container for data fields - an xml element which has DataField + elements as children.''' + def __init__(self, fields=None, associated=None, extend=None): self.associated = associated + self.vars = {} + if extend is None: + # we have to build this object from scratch + xmpp.Node.__init__(self) - xmpp.Node.__init__(self, node=node) + if fields is not None: self.fields = fields + else: + # we already have xmpp.Node inside - try to convert all + # fields into DataField objects + if fields is None: + for field in self.getTags('field'): # TODO: iter! + if not isinstance(field, DataField): + ExtendField(field) + self.vars[field.var] = field + else: + for field in self.iterTags('field'): + self.delChild(field) + self.fields = fields - if node is not None: - for field in iter_elements(node, 'field'): - assert isinstance(field, DataField) - self.vars[field.var] = field - - if fields is not None: + @nested_property + def fields(): + '''List of fields in this record.''' + def fget(self): + return self.getTags('field') + def fset(self, fields): + fdel(self) for field in fields: - assert isinstance(field, DataField) + if not isinstance(field, DataField): + ExtendField(extend=field) self.addChild(node=field) - self.vars[field.var] = field - - # if there will be ever needed access to all fields as a list, write it here, in form of property + def fdel(self): + for element in self.iterTags('field'): + self.delChild(element) + return locals() def iter_fields(self): - if self.associated is not None: - for field in self.associated.iter_fields(): - if field.var in self.vars: - yield self.vars[field.var] - else: - yield None - else: - for field in self.vars.itervalues(): - yield field + ''' Iterate over fields in this record. Do not take associated + into account. ''' + for field in self.iterTags('field'): + yield field + + def iter_with_associated(self): + ''' Iterate over associated, yielding both our field and + associated one together. ''' + for field in self.associated.iter_fields(): + yield self[field.var], field def __getitem__(self, item): - # if the field is in associated but not here - return None - # NOTE: last predicate is meant to raise KeyError if no such item in associated - if self.associated is not None and item not in self.vars and \ - self.associated[item] is not None: - return None return self.vars[item] + +class DataForm(ExtendedNode): + def __init__(self, type=None, title=None, instructions=None, extend=None): + if extend is None: + # we have to build form from scratch + xmpp.Node.__init__(self, 'x', attrs={'xmlns': xmpp.NS_DATA}) + + if type is not None: self.type=type + if title is not None: self.title=title + if instructions is not None: self.instructions=instructions + + @nested_property + def type(): + ''' Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'. + 'form' - this form is to be filled in; you can do: + filledform = DataForm(replyto=thisform)...''' + def fget(self): + return self.getAttr('type') + def fset(self): + assert type in ('form', 'submit', 'cancel', 'result') + self.setAttr('type', type) + return locals() + + @nested_property + def title(): + ''' Title of the form. Human-readable, should not contain any \\r\\n.''' + def fget(self): + return self.getTagData('title') + def fset(self, title): + self.setTagData('title', title) + def fdel(self): + try: + self.delChild('title') + except ValueError: + pass + return locals() + + @nested_property + def instructions(): + ''' Instructions for this form. Human-readable, may contain \\r\\n. ''' + # TODO: the same code is in TextMultiField. join them + def fget(self): + value = u'' + for value in self.iterTags('value'): + value += '\n' + value.getData() + return value[1:] + def fset(self, value): + fdel(self) + if value == '': return + for line in value.split('\n'): + self.addChild('value').setData(line) + def fdel(self): + for value in self.iterTags('value'): + self.delChild(value) + return locals() + +class SimpleDataForm(DataForm, DataRecord): + def __init__(self, type=None, title=None, instructions=None, fields=None, extend=None): + DataForm.__init__(self, type=type, title=title, instructions=instructions, extend=extend) + DataRecord.__init__(self, fields=fields, extend=self, associated=self) + +class MultipleDataForm(DataForm): + def __init__(self): + # all records, recorded into DataRecords + pass + + @nested_property + def items(): + ''' A list of all records. ''' + def fget(self): + return list(self.iter_records()) + def fset(self, records): + fdel(self) + for record in records: + if not isinstance(record, DataRecord): + DataRecord(extend=record) + self.addChild(node=record) + def fdel(self): + for record in self.iterTags('record'): + self.delChild(record) + return locals() + + def iter_records(): + for record in self.iterTags('item'): + yield item + + @nested_property + def recorded(): + ''' DataRecord that contains descriptions of fields in records.''' + def fget(self): + return self.getTag('recorded') + def fset(self, record): + try: + self.delChild('recorded') + except: + pass + + record.setName('recorded') + self.addChild(node=record) + return locals() + diff --git a/src/common/xmpp/simplexml.py b/src/common/xmpp/simplexml.py index b26d2ec3f..af304af80 100644 --- a/src/common/xmpp/simplexml.py +++ b/src/common/xmpp/simplexml.py @@ -33,7 +33,7 @@ def ustr(what): if type(r)<>type(u''): return unicode(r,ENCODING) return r -class Node: +class Node(object): """ Node class describes syntax of separate XML Node. It have a constructor that permits node creation from set of "namespace name", attributes and payload of text strings and other nodes. It does not natively support building node from text string and uses NodeBuilder class for that purpose.