gajim-plural/src/common/dataforms.py

466 lines
13 KiB
Python
Raw Normal View History

# 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):
@classmethod
def __new__(cls, *a, **b):
2007-06-09 15:52:06 +02:00
if 'extend' not in b.keys() or not b['extend']:
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):
''' Helper function to create a field of given type. '''
f = {
'boolean': BooleanField,
'fixed': TextMultiField, # not editable, still can have multiple lines of text
'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
def ExtendField(node):
''' Helper function to extend a node to field of appropriate type. '''
# when validation (XEP-122) will go in, we could have another classes
# like DateTimeField - so that dicts in Field() and ExtendField() will
# be different...
typ=node.getAttr('type')
f = {
'boolean': BooleanField,
'fixed': TextMultiField,
'hidden': StringField,
'text-private': StringField,
'text-single': StringField,
'jid-multi': ListMultiField,
'jid-single': ListSingleField,
'list-multi': ListMultiField,
'list-single': ListSingleField,
'text-multi': TextMultiField,
}
if typ not in f:
typ = 'text-single'
return f[typ](extend=node)
def ExtendForm(node):
''' Helper function to extend a node to form of appropriate type. '''
if node.getTag('reported') is not None:
return MultipleDataForm(extend=node)
else:
return SimpleDataForm(extend=node)
class DataField(ExtendedNode):
2007-06-27 02:51:38 +02:00
""" Keeps data about one field - var, field type, labels, instructions...
Base class for different kinds of fields. Use Field() function to
construct one of these. """
def __init__(self, typ=None, var=None, value=None, label=None, desc=None, required=False,
options=None, extend=None):
if extend is None:
ExtendedNode.__init__(self, 'field')
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
@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):
if self.getAttr('label'):
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):
2006-11-20 20:34:11 +01:00
return bool(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 False # default value is False
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):
try:
self.delChild(self.getTag('value'))
except ValueError: # if there already were no value tag
pass
return locals()
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.getTags('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.getTags('option'):
self.delChild(element)
return locals()
def iter_options(self):
for element in self.iterTags('option'):
v = element.getTagData('value')
if v is None: raise WrongFieldValue
yield (v, element.getAttr('label'))
class ListSingleField(ListField, StringField):
'''Covers list-single and jid-single fields.'''
pass
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.getTags('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.getTags('value'):
self.delChild(element)
return locals()
2006-11-20 20:59:07 +01:00
def iter_values(self):
for element in self.getTags('value'):
yield element.getData()
class TextMultiField(DataField):
@nested_property
def value():
'''Value held in field.'''
def fget(self):
value = u''
for element in self.iterTags('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 self.getTags('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
if fields is None:
for field in self.iterTags('field'):
if not isinstance(field, DataField):
ExtendField(field)
self.vars[field.var] = field
else:
for field in self.getTags('field'):
self.delChild(field)
self.fields = fields
@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):
ExtendField(extend=field)
self.addChild(node=field)
def fdel(self):
for element in self.getTags('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, 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 will be able soon to do:
filledform = DataForm(replyto=thisform)...'''
def fget(self):
return self.getAttr('type')
def fset(self, type):
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''
2007-02-15 10:33:30 +01:00
for valuenode in self.getTags('instructions'):
2006-11-19 19:30:58 +01:00
value += '\n' + valuenode.getData()
return value[1:]
def fset(self, value):
fdel(self)
if value == '': return
for line in value.split('\n'):
2007-02-15 10:33:30 +01:00
self.addChild('instructions').setData(line)
def fdel(self):
2007-02-15 10:33:30 +01:00
for value in self.getTags('instructions'):
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)
def get_purged(self):
c = SimpleDataForm(extend=self)
del c.title
c.instructions = ''
to_be_removed = []
for f in c.iter_fields():
if f.required:
# Keep all required fields
continue
if (hasattr(f, 'value') and not f.value) or (hasattr(f, 'values') and \
len(f.values) == 0):
to_be_removed.append(f)
else:
del f.label
del f.description
for f in to_be_removed:
c.delChild(f)
return c
class MultipleDataForm(DataForm):
def __init__(self, type=None, title=None, instructions=None, items=None, extend=None):
DataForm.__init__(self, type=type, title=title, instructions=instructions, extend=extend)
# all records, recorded into DataRecords
if extend is None:
if items is not None: self.items = items
else:
# we already have xmpp.Node inside - try to convert all
# fields into DataField objects
if items is None:
self.items = list(self.iterTags('item'))
else:
for item in self.getTags('item'):
self.delChild(item)
self.items = items
reported_tag = self.getTag('reported')
self.reported = DataRecord(extend = reported_tag)
@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.getTags('item'):
self.delChild(record)
return locals()
2006-11-20 20:34:11 +01:00
def iter_records(self):
for record in self.getTags('item'):
2006-11-20 20:34:11 +01:00
yield record
# @nested_property
# def reported():
# ''' DataRecord that contains descriptions of fields in records.'''
# def fget(self):
# return self.getTag('reported')
# def fset(self, record):
# try:
# self.delChild('reported')
# except:
# pass
#
# record.setName('reported')
# self.addChild(node=record)
# return locals()
# vim: se ts=3: