# this will go to src/common/xmpp later, for now it is in src/common # -*- coding:utf-8 -*- ## src/common/dataforms.py ## ## Copyright (C) 2006-2007 Tomasz Melcer ## Copyright (C) 2006-2010 Yann Leboulanger ## Copyright (C) 2007 Stephan Erb ## ## 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 . ## """ This module contains wrappers for different parts of data forms (JEP 0004). For information how to use them, read documentation """ import xmpp import helpers # exceptions used in this module # base class class Error(Exception): pass # when we get xmpp.Node which we do not understand class UnknownDataForm(Error): pass # when we get xmpp.Node which contains bad fields class WrongFieldValue(Error): pass # 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() 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': 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 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': 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 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): """ 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): l = self.getAttr('label') if not l: l = self.var return l 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): 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() @nested_property def media(): """ Media data """ def fget(self): media = self.getTag('media', namespace=xmpp.NS_DATA_MEDIA) if media: return Media(media) def fset(self, value): fdel(self) self.addChild(node=value) def fdel(self): t = self.getTag('media') if t is not None: self.delChild(t) return locals() def is_valid(self): return True class Uri(xmpp.Node): def __init__(self, uri_tag): xmpp.Node.__init__(self, node=uri_tag) @nested_property def type_(): """ uri type """ def fget(self): return self.getAttr('type') def fset(self, value): self.setAttr('type', value) def fdel(self): self.delAttr('type') return locals() @nested_property def uri_data(): """ uri data """ def fget(self): return self.getData() def fset(self, value): self.setData(value) def fdel(self): self.setData(None) return locals() class Media(xmpp.Node): def __init__(self, media_tag): xmpp.Node.__init__(self, node=media_tag) @nested_property def uris(): """ URIs of the media element. """ def fget(self): return map(Uri, self.getTags('uri')) def fset(self, value): fdel(self) for uri in values: self.addChild(node=uri) def fdel(self, value): for element in self.getTags('uri'): self.delChild(element) 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 == '' and not self.required: 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 l = element.getAttr('label') if not l: l = v options.append((l, 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 l = element.getAttr('label') if not l: l = v yield (v, l) class ListSingleField(ListField, StringField): """ Covers list-single field """ pass class JidSingleField(ListSingleField): """ Covers jid-single fields """ def is_valid(self): if self.value: try: helpers.parse_jid(self.value) return True except: return False if self.required: return False return True class ListMultiField(ListField): """ Covers list-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() def iter_values(self): for element in self.getTags('value'): yield element.getData() class JidMultiField(ListMultiField): """ Covers jid-multi fields """ def is_valid(self): if len(self.values): for value in self.values: try: helpers.parse_jid(self.value) except: return False return True if self.required: return False return True 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] def is_valid(self): for f in self.iter_fields(): if not f.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 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'' for valuenode in self.getTags('instructions'): value += '\n' + valuenode.getData() return value[1:] def fset(self, value): fdel(self) if value == '': return for line in value.split('\n'): self.addChild('instructions').setData(line) def fdel(self): 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: # add if there is not if hasattr(f, 'value') and not f.value: f.value = '' if hasattr(f, 'values') and not f.values: f.values = [''] # 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 del f.media 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() def iter_records(self): for record in self.getTags('item'): 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()