gajim-plural/gajim/common/modules/dataforms.py

749 lines
20 KiB
Python

# Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
# Copyright (C) 2006-2014 Yann Leboulanger <asterix AT lagaule.org>
# Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
#
# This file is part of Gajim.
#
# Gajim is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation; version 3 only.
#
# Gajim is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Gajim. If not, see <http://www.gnu.org/licenses/>.
# XEP-0004: Data Forms
import nbxmpp
from gajim.common import helpers
# exceptions used in this module
class Error(Exception):
pass
# when we get nbxmpp.Node which we do not understand
class UnknownDataForm(Error):
pass
# when we get nbxmpp.Node which contains bad fields
class WrongFieldValue(Error):
pass
# helper class to change class of already existing object
class ExtendedNode(nbxmpp.Node):
@classmethod
def __new__(cls, *args, **kwargs):
if 'extend' not in kwargs.keys() or not kwargs['extend']:
return object.__new__(cls)
extend = kwargs['extend']
assert issubclass(cls, extend.__class__)
extend.__class__ = cls
return extend
# helper to create fields from scratch
def create_field(typ, **attrs):
''' Helper function to create a field of given type. '''
field = {
'boolean': BooleanField,
'fixed': StringField,
'hidden': StringField,
'text-private': StringField,
'text-single': StringField,
'jid-multi': JidMultiField,
'jid-single': JidSingleField,
'list-multi': ListMultiField,
'list-single': ListSingleField,
'text-multi': TextMultiField,
}[typ](typ=typ, **attrs)
return field
def extend_field(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 create_field() and
# extend_field() will be different...
typ = node.getAttr('type')
field = {
'boolean': BooleanField,
'fixed': StringField,
'hidden': StringField,
'text-private': StringField,
'text-single': StringField,
'jid-multi': JidMultiField,
'jid-single': JidSingleField,
'list-multi': ListMultiField,
'list-single': ListSingleField,
'text-multi': TextMultiField,
}
if typ not in field:
typ = 'text-single'
return field[typ](extend=node)
def extend_form(node):
"""
Helper function to extend a node to form of appropriate type
"""
if node.getTag('reported') is not None:
return MultipleDataForm(extend=node)
return SimpleDataForm(extend=node)
class DataField(ExtendedNode):
"""
Keeps data about one field - var, field type, labels, instructions... Base
class for different kinds of fields. Use create_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
@property
def type_(self):
"""
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
"""
type_ = self.getAttr('type')
if type_ is None:
return 'text-single'
return type_
@type_.setter
def type_(self, value):
assert isinstance(value, str)
self.setAttr('type', value)
@property
def var(self):
"""
Field identifier
"""
return self.getAttr('var')
@var.setter
def var(self, value):
assert isinstance(value, str)
self.setAttr('var', value)
@var.deleter
def var(self):
self.delAttr('var')
@property
def label(self):
"""
Human-readable field name
"""
label_ = self.getAttr('label')
if not label_:
label_ = self.var
return label_
@label.setter
def label(self, value):
assert isinstance(value, str)
self.setAttr('label', value)
@label.deleter
def label(self):
if self.getAttr('label'):
self.delAttr('label')
@property
def description(self):
"""
Human-readable description of field meaning
"""
return self.getTagData('desc') or ''
@description.setter
def description(self, value):
assert isinstance(value, str)
if value == '':
del self.description
else:
self.setTagData('desc', value)
@description.deleter
def description(self):
desc = self.getTag('desc')
if desc is not None:
self.delChild(desc)
@property
def required(self):
"""
Controls whether this field required to fill. Boolean
"""
return bool(self.getTag('required'))
@required.setter
def required(self, value):
required = self.getTag('required')
if required and not value:
self.delChild(required)
elif not required and value:
self.addChild('required')
@property
def media(self):
"""
Media data
"""
media = self.getTag('media', namespace=nbxmpp.NS_DATA_MEDIA)
if media:
return Media(media)
@media.setter
def media(self, value):
del self.media
self.addChild(node=value)
@media.deleter
def media(self):
media = self.getTag('media')
if media is not None:
self.delChild(media)
@staticmethod
def is_valid():
return True
class Uri(nbxmpp.Node):
def __init__(self, uri_tag):
nbxmpp.Node.__init__(self, node=uri_tag)
@property
def type_(self):
"""
uri type
"""
return self.getAttr('type')
@type_.setter
def type_(self, value):
self.setAttr('type', value)
@type_.deleter
def type_(self):
self.delAttr('type')
@property
def uri_data(self):
"""
uri data
"""
return self.getData()
@uri_data.setter
def uri_data(self, value):
self.setData(value)
@uri_data.deleter
def uri_data(self):
self.setData(None)
class Media(nbxmpp.Node):
def __init__(self, media_tag):
nbxmpp.Node.__init__(self, node=media_tag)
@property
def uris(self):
"""
URIs of the media element.
"""
return map(Uri, self.getTags('uri'))
@uris.setter
def uris(self, value):
del self.uris
for uri in value:
self.addChild(node=uri)
@uris.deleter
def uris(self):
for element in self.getTags('uri'):
self.delChild(element)
class BooleanField(DataField):
@property
def value(self):
"""
Value of field. May contain True, False or None
"""
value = self.getTagData('value')
if value in ('0', 'false'):
return False
if value in ('1', 'true'):
return True
if value is None:
return False # default value is False
raise WrongFieldValue
@value.setter
def value(self, value):
self.setTagData('value', value and '1' or '0')
@value.deleter
def value(self):
value = self.getTag('value')
if value is not None:
self.delChild(value)
class StringField(DataField):
"""
Covers fields of types: fixed, hidden, text-private, text-single
"""
@property
def value(self):
"""
Value of field. May be any string
"""
return self.getTagData('value') or ''
@value.setter
def value(self, value):
assert isinstance(value, str)
if value == '' and not self.required:
del self.value
return
self.setTagData('value', value)
@value.deleter
def value(self):
try:
self.delChild(self.getTag('value'))
except ValueError: # if there already were no value tag
pass
class ListField(DataField):
"""
Covers fields of types: jid-multi, jid-single, list-multi, list-single
"""
@property
def options(self):
"""
Options
"""
options = []
for element in self.getTags('option'):
value = element.getTagData('value')
if value is None:
raise WrongFieldValue
label = element.getAttr('label')
if not label:
label = value
options.append((label, value))
return options
@options.setter
def options(self, values):
del self.options
for value, label in values:
self.addChild('option',
{'label': label}).setTagData('value', value)
@options.deleter
def options(self):
for element in self.getTags('option'):
self.delChild(element)
def iter_options(self):
for element in self.iterTags('option'):
value = element.getTagData('value')
if value is None:
raise WrongFieldValue
label = element.getAttr('label')
if not label:
label = value
yield (value, label)
class ListSingleField(ListField, StringField):
"""
Covers list-single field
"""
def is_valid(self):
if not self.required:
return True
if not self.value:
return False
return True
class JidSingleField(ListSingleField):
"""
Covers jid-single fields
"""
def is_valid(self):
if self.value:
try:
helpers.parse_jid(self.value)
return True
except Exception:
return False
if self.required:
return False
return True
class ListMultiField(ListField):
"""
Covers list-multi fields
"""
@property
def values(self):
"""
Values held in field
"""
values = []
for element in self.getTags('value'):
values.append(element.getData())
return values
@values.setter
def values(self, values):
del self.values
for value in values:
self.addChild('value').setData(value)
@values.deleter
def values(self):
for element in self.getTags('value'):
self.delChild(element)
def iter_values(self):
for element in self.getTags('value'):
yield element.getData()
def is_valid(self):
if not self.required:
return True
if not self.values:
return False
return True
class JidMultiField(ListMultiField):
"""
Covers jid-multi fields
"""
def is_valid(self):
if self.values:
for value in self.values:
try:
helpers.parse_jid(value)
except Exception:
return False
return True
if self.required:
return False
return True
class TextMultiField(DataField):
@property
def value(self):
"""
Value held in field
"""
value = ''
for element in self.iterTags('value'):
value += '\n' + element.getData()
return value[1:]
@value.setter
def value(self, value):
del self.value
if value == '':
return
for line in value.split('\n'):
self.addChild('value').setData(line)
@value.deleter
def value(self):
for element in self.getTags('value'):
self.delChild(element)
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
nbxmpp.Node.__init__(self)
if fields is not None:
self.fields = fields
else:
# we already have nbxmpp.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):
extend_field(field)
self.vars[field.var] = field
else:
for field in self.getTags('field'):
self.delChild(field)
self.fields = fields
@property
def fields(self):
"""
List of fields in this record
"""
return self.getTags('field')
@fields.setter
def fields(self, fields):
del self.fields
for field in fields:
if not isinstance(field, DataField):
extend_field(field)
self.addChild(node=field)
@fields.deleter
def fields(self):
for element in self.getTags('field'):
self.delChild(element)
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]
def is_valid(self):
for field in self.iter_fields():
if not field.is_valid():
return False
return True
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
nbxmpp.Node.__init__(self, 'x', attrs={'xmlns': nbxmpp.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
@property
def type_(self):
"""
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)
"""
return self.getAttr('type')
@type_.setter
def type_(self, type_):
assert type_ in ('form', 'submit', 'cancel', 'result')
self.setAttr('type', type_)
@property
def title(self):
"""
Title of the form
Human-readable, should not contain any \\r\\n.
"""
return self.getTagData('title')
@title.setter
def title(self, title):
self.setTagData('title', title)
@title.deleter
def title(self):
try:
self.delChild('title')
except ValueError:
pass
@property
def instructions(self):
"""
Instructions for this form
Human-readable, may contain \\r\\n.
"""
# TODO: the same code is in TextMultiField. join them
value = ''
for valuenode in self.getTags('instructions'):
value += '\n' + valuenode.getData()
return value[1:]
@instructions.setter
def instructions(self, value):
del self.instructions
if value == '':
return
for line in value.split('\n'):
self.addChild('instructions').setData(line)
@instructions.deleter
def instructions(self):
for value in self.getTags('instructions'):
self.delChild(value)
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):
simple_form = SimpleDataForm(extend=self)
del simple_form.title
simple_form.instructions = ''
to_be_removed = []
for field in simple_form.iter_fields():
if field.required:
# add <value> if there is not
if hasattr(field, 'value') and not field.value:
field.value = ''
# Keep all required fields
continue
if ((hasattr(field, 'value') and
not field.value and
field.value != 0) or
(hasattr(field, 'values') and not field.values)):
to_be_removed.append(field)
else:
del field.label
del field.description
del field.media
for field in to_be_removed:
simple_form.delChild(field)
return simple_form
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 nbxmpp.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)
@property
def items(self):
"""
A list of all records
"""
return list(self.iter_records())
@items.setter
def items(self, records):
del self.items
for record in records:
if not isinstance(record, DataRecord):
DataRecord(extend=record)
self.addChild(node=record)
@items.deleter
def items(self):
for record in self.getTags('item'):
self.delChild(record)
def iter_records(self):
for record in self.getTags('item'):
yield record
# @property
# def reported(self):
# """
# DataRecord that contains descriptions of fields in records
# """
# return self.getTag('reported')
#
# @reported.setter
# def reported(self, record):
# try:
# self.delChild('reported')
# except Exception:
# pass
#
# record.setName('reported')
# self.addChild(node=record)