don't open twice the same subscription request dialog. see #6762
[gajim.git] / src / common / dataforms.py
blob8cc5cc2b99a21676e7cd8ff05f9d10683daa75f7
1 # this will go to src/common/xmpp later, for now it is in src/common
2 # -*- coding:utf-8 -*-
3 ## src/common/dataforms.py
4 ##
5 ## Copyright (C) 2006-2007 Tomasz Melcer <liori AT exroot.org>
6 ## Copyright (C) 2006-2010 Yann Leboulanger <asterix AT lagaule.org>
7 ## Copyright (C) 2007 Stephan Erb <steve-e AT h3c.de>
8 ##
9 ## This file is part of Gajim.
11 ## Gajim is free software; you can redistribute it and/or modify
12 ## it under the terms of the GNU General Public License as published
13 ## by the Free Software Foundation; version 3 only.
15 ## Gajim is distributed in the hope that it will be useful,
16 ## but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ## GNU General Public License for more details.
20 ## You should have received a copy of the GNU General Public License
21 ## along with Gajim. If not, see <http://www.gnu.org/licenses/>.
24 """
25 This module contains wrappers for different parts of data forms (JEP 0004). For
26 information how to use them, read documentation
27 """
29 import xmpp
30 import helpers
32 # exceptions used in this module
33 # base class
34 class Error(Exception): pass
35 # when we get xmpp.Node which we do not understand
36 class UnknownDataForm(Error): pass
37 # when we get xmpp.Node which contains bad fields
38 class WrongFieldValue(Error): pass
40 # helper class to change class of already existing object
41 class ExtendedNode(xmpp.Node, object):
42 @classmethod
43 def __new__(cls, *a, **b):
44 if 'extend' not in b.keys() or not b['extend']:
45 return object.__new__(cls)
47 extend = b['extend']
48 assert issubclass(cls, extend.__class__)
49 extend.__class__ = cls
50 return extend
52 # helper decorator to create properties in cleaner way
53 def nested_property(f):
54 ret = f()
55 p = {'doc': f.__doc__}
56 for v in ('fget', 'fset', 'fdel', 'doc'):
57 if v in ret.keys(): p[v]=ret[v]
58 return property(**p)
60 # helper to create fields from scratch
61 def Field(typ, **attrs):
62 ''' Helper function to create a field of given type. '''
63 f = {
64 'boolean': BooleanField,
65 'fixed': StringField,
66 'hidden': StringField,
67 'text-private': StringField,
68 'text-single': StringField,
69 'jid-multi': JidMultiField,
70 'jid-single': JidSingleField,
71 'list-multi': ListMultiField,
72 'list-single': ListSingleField,
73 'text-multi': TextMultiField,
74 }[typ](typ=typ, **attrs)
75 return f
77 def ExtendField(node):
78 """
79 Helper function to extend a node to field of appropriate type
80 """
81 # when validation (XEP-122) will go in, we could have another classes
82 # like DateTimeField - so that dicts in Field() and ExtendField() will
83 # be different...
84 typ=node.getAttr('type')
85 f = {
86 'boolean': BooleanField,
87 'fixed': StringField,
88 'hidden': StringField,
89 'text-private': StringField,
90 'text-single': StringField,
91 'jid-multi': JidMultiField,
92 'jid-single': JidSingleField,
93 'list-multi': ListMultiField,
94 'list-single': ListSingleField,
95 'text-multi': TextMultiField,
97 if typ not in f:
98 typ = 'text-single'
99 return f[typ](extend=node)
101 def ExtendForm(node):
103 Helper function to extend a node to form of appropriate type
105 if node.getTag('reported') is not None:
106 return MultipleDataForm(extend=node)
107 else:
108 return SimpleDataForm(extend=node)
110 class DataField(ExtendedNode):
112 Keeps data about one field - var, field type, labels, instructions... Base
113 class for different kinds of fields. Use Field() function to construct one
114 of these
117 def __init__(self, typ=None, var=None, value=None, label=None, desc=None,
118 required=False, options=None, extend=None):
120 if extend is None:
121 ExtendedNode.__init__(self, 'field')
123 self.type = typ
124 self.var = var
125 if value is not None:
126 self.value = value
127 if label is not None:
128 self.label = label
129 if desc is not None:
130 self.desc = desc
131 self.required = required
132 self.options = options
134 @nested_property
135 def type():
137 Type of field. Recognized values are: 'boolean', 'fixed', 'hidden',
138 'jid-multi', 'jid-single', 'list-multi', 'list-single', 'text-multi',
139 'text-private', 'text-single'. If you set this to something different,
140 DataField will store given name, but treat all data as text-single
142 def fget(self):
143 t = self.getAttr('type')
144 if t is None:
145 return 'text-single'
146 return t
148 def fset(self, value):
149 assert isinstance(value, basestring)
150 self.setAttr('type', value)
152 return locals()
154 @nested_property
155 def var():
157 Field identifier
159 def fget(self):
160 return self.getAttr('var')
162 def fset(self, value):
163 assert isinstance(value, basestring)
164 self.setAttr('var', value)
166 def fdel(self):
167 self.delAttr('var')
169 return locals()
171 @nested_property
172 def label():
174 Human-readable field name
176 def fget(self):
177 l = self.getAttr('label')
178 if not l:
179 l = self.var
180 return l
182 def fset(self, value):
183 assert isinstance(value, basestring)
184 self.setAttr('label', value)
186 def fdel(self):
187 if self.getAttr('label'):
188 self.delAttr('label')
190 return locals()
192 @nested_property
193 def description():
195 Human-readable description of field meaning
197 def fget(self):
198 return self.getTagData('desc') or u''
200 def fset(self, value):
201 assert isinstance(value, basestring)
202 if value == '':
203 fdel(self)
204 else:
205 self.setTagData('desc', value)
207 def fdel(self):
208 t = self.getTag('desc')
209 if t is not None:
210 self.delChild(t)
212 return locals()
214 @nested_property
215 def required():
217 Controls whether this field required to fill. Boolean
219 def fget(self):
220 return bool(self.getTag('required'))
222 def fset(self, value):
223 t = self.getTag('required')
224 if t and not value:
225 self.delChild(t)
226 elif not t and value:
227 self.addChild('required')
229 return locals()
231 @nested_property
232 def media():
234 Media data
236 def fget(self):
237 media = self.getTag('media', namespace=xmpp.NS_DATA_MEDIA)
238 if media:
239 return Media(media)
241 def fset(self, value):
242 fdel(self)
243 self.addChild(node=value)
245 def fdel(self):
246 t = self.getTag('media')
247 if t is not None:
248 self.delChild(t)
250 return locals()
252 def is_valid(self):
253 return True
255 class Uri(xmpp.Node):
256 def __init__(self, uri_tag):
257 xmpp.Node.__init__(self, node=uri_tag)
259 @nested_property
260 def type_():
262 uri type
264 def fget(self):
265 return self.getAttr('type')
267 def fset(self, value):
268 self.setAttr('type', value)
270 def fdel(self):
271 self.delAttr('type')
273 return locals()
275 @nested_property
276 def uri_data():
278 uri data
280 def fget(self):
281 return self.getData()
283 def fset(self, value):
284 self.setData(value)
286 def fdel(self):
287 self.setData(None)
289 return locals()
291 class Media(xmpp.Node):
292 def __init__(self, media_tag):
293 xmpp.Node.__init__(self, node=media_tag)
295 @nested_property
296 def uris():
298 URIs of the media element.
300 def fget(self):
301 return map(Uri, self.getTags('uri'))
303 def fset(self, value):
304 fdel(self)
305 for uri in values:
306 self.addChild(node=uri)
308 def fdel(self, value):
309 for element in self.getTags('uri'):
310 self.delChild(element)
312 return locals()
314 class BooleanField(DataField):
315 @nested_property
316 def value():
318 Value of field. May contain True, False or None
320 def fget(self):
321 v = self.getTagData('value')
322 if v in ('0', 'false'):
323 return False
324 if v in ('1', 'true'):
325 return True
326 if v is None:
327 return False # default value is False
328 raise WrongFieldValue
330 def fset(self, value):
331 self.setTagData('value', value and '1' or '0')
333 def fdel(self, value):
334 t = self.getTag('value')
335 if t is not None:
336 self.delChild(t)
338 return locals()
340 class StringField(DataField):
342 Covers fields of types: fixed, hidden, text-private, text-single
345 @nested_property
346 def value():
348 Value of field. May be any unicode string
350 def fget(self):
351 return self.getTagData('value') or u''
353 def fset(self, value):
354 assert isinstance(value, basestring)
355 if value == '' and not self.required:
356 return fdel(self)
357 self.setTagData('value', value)
359 def fdel(self):
360 try:
361 self.delChild(self.getTag('value'))
362 except ValueError: # if there already were no value tag
363 pass
365 return locals()
367 class ListField(DataField):
369 Covers fields of types: jid-multi, jid-single, list-multi, list-single
372 @nested_property
373 def options():
375 Options
377 def fget(self):
378 options = []
379 for element in self.getTags('option'):
380 v = element.getTagData('value')
381 if v is None:
382 raise WrongFieldValue
383 l = element.getAttr('label')
384 if not l:
385 l = v
386 options.append((l, v))
387 return options
389 def fset(self, values):
390 fdel(self)
391 for value, label in values:
392 self.addChild('option', {'label': label}).setTagData('value', value)
394 def fdel(self):
395 for element in self.getTags('option'):
396 self.delChild(element)
398 return locals()
400 def iter_options(self):
401 for element in self.iterTags('option'):
402 v = element.getTagData('value')
403 if v is None:
404 raise WrongFieldValue
405 l = element.getAttr('label')
406 if not l:
407 l = v
408 yield (v, l)
410 class ListSingleField(ListField, StringField):
412 Covers list-single field
414 def is_valid(self):
415 if not self.required:
416 return True
417 if not self.value:
418 return False
419 return True
421 class JidSingleField(ListSingleField):
423 Covers jid-single fields
425 def is_valid(self):
426 if self.value:
427 try:
428 helpers.parse_jid(self.value)
429 return True
430 except:
431 return False
432 if self.required:
433 return False
434 return True
436 class ListMultiField(ListField):
438 Covers list-multi fields
441 @nested_property
442 def values():
444 Values held in field
446 def fget(self):
447 values = []
448 for element in self.getTags('value'):
449 values.append(element.getData())
450 return values
452 def fset(self, values):
453 fdel(self)
454 for value in values:
455 self.addChild('value').setData(value)
457 def fdel(self):
458 for element in self.getTags('value'):
459 self.delChild(element)
461 return locals()
463 def iter_values(self):
464 for element in self.getTags('value'):
465 yield element.getData()
467 def is_valid(self):
468 if not self.required:
469 return True
470 if not self.values:
471 return False
472 return True
474 class JidMultiField(ListMultiField):
476 Covers jid-multi fields
478 def is_valid(self):
479 if len(self.values):
480 for value in self.values:
481 try:
482 helpers.parse_jid(value)
483 except:
484 return False
485 return True
486 if self.required:
487 return False
488 return True
490 class TextMultiField(DataField):
491 @nested_property
492 def value():
494 Value held in field
496 def fget(self):
497 value = u''
498 for element in self.iterTags('value'):
499 value += '\n' + element.getData()
500 return value[1:]
502 def fset(self, value):
503 fdel(self)
504 if value == '':
505 return
506 for line in value.split('\n'):
507 self.addChild('value').setData(line)
509 def fdel(self):
510 for element in self.getTags('value'):
511 self.delChild(element)
513 return locals()
515 class DataRecord(ExtendedNode):
517 The container for data fields - an xml element which has DataField elements
518 as children
520 def __init__(self, fields=None, associated=None, extend=None):
521 self.associated = associated
522 self.vars = {}
523 if extend is None:
524 # we have to build this object from scratch
525 xmpp.Node.__init__(self)
527 if fields is not None:
528 self.fields = fields
529 else:
530 # we already have xmpp.Node inside - try to convert all
531 # fields into DataField objects
532 if fields is None:
533 for field in self.iterTags('field'):
534 if not isinstance(field, DataField):
535 ExtendField(field)
536 self.vars[field.var] = field
537 else:
538 for field in self.getTags('field'):
539 self.delChild(field)
540 self.fields = fields
542 @nested_property
543 def fields():
545 List of fields in this record
547 def fget(self):
548 return self.getTags('field')
550 def fset(self, fields):
551 fdel(self)
552 for field in fields:
553 if not isinstance(field, DataField):
554 ExtendField(extend=field)
555 self.addChild(node=field)
557 def fdel(self):
558 for element in self.getTags('field'):
559 self.delChild(element)
561 return locals()
563 def iter_fields(self):
565 Iterate over fields in this record. Do not take associated into account
567 for field in self.iterTags('field'):
568 yield field
570 def iter_with_associated(self):
572 Iterate over associated, yielding both our field and associated one
573 together
575 for field in self.associated.iter_fields():
576 yield self[field.var], field
578 def __getitem__(self, item):
579 return self.vars[item]
581 def is_valid(self):
582 for f in self.iter_fields():
583 if not f.is_valid():
584 return False
585 return True
587 class DataForm(ExtendedNode):
588 def __init__(self, type_=None, title=None, instructions=None, extend=None):
589 if extend is None:
590 # we have to build form from scratch
591 xmpp.Node.__init__(self, 'x', attrs={'xmlns': xmpp.NS_DATA})
593 if type_ is not None:
594 self.type_=type_
595 if title is not None:
596 self.title=title
597 if instructions is not None:
598 self.instructions=instructions
600 @nested_property
601 def type():
603 Type of the form. Must be one of: 'form', 'submit', 'cancel', 'result'.
604 'form' - this form is to be filled in; you will be able soon to do:
605 filledform = DataForm(replyto=thisform)
607 def fget(self):
608 return self.getAttr('type')
610 def fset(self, type_):
611 assert type_ in ('form', 'submit', 'cancel', 'result')
612 self.setAttr('type', type_)
614 return locals()
616 @nested_property
617 def title():
619 Title of the form
621 Human-readable, should not contain any \\r\\n.
623 def fget(self):
624 return self.getTagData('title')
626 def fset(self, title):
627 self.setTagData('title', title)
629 def fdel(self):
630 try:
631 self.delChild('title')
632 except ValueError:
633 pass
635 return locals()
637 @nested_property
638 def instructions():
640 Instructions for this form
642 Human-readable, may contain \\r\\n.
644 # TODO: the same code is in TextMultiField. join them
645 def fget(self):
646 value = u''
647 for valuenode in self.getTags('instructions'):
648 value += '\n' + valuenode.getData()
649 return value[1:]
651 def fset(self, value):
652 fdel(self)
653 if value == '': return
654 for line in value.split('\n'):
655 self.addChild('instructions').setData(line)
657 def fdel(self):
658 for value in self.getTags('instructions'):
659 self.delChild(value)
661 return locals()
663 class SimpleDataForm(DataForm, DataRecord):
664 def __init__(self, type_=None, title=None, instructions=None, fields=None, \
665 extend=None):
666 DataForm.__init__(self, type_=type_, title=title,
667 instructions=instructions, extend=extend)
668 DataRecord.__init__(self, fields=fields, extend=self, associated=self)
670 def get_purged(self):
671 c = SimpleDataForm(extend=self)
672 del c.title
673 c.instructions = ''
674 to_be_removed = []
675 for f in c.iter_fields():
676 if f.required:
677 # add <value> if there is not
678 if hasattr(f, 'value') and not f.value:
679 f.value = ''
680 # Keep all required fields
681 continue
682 if (hasattr(f, 'value') and not f.value and f.value != 0) or (
683 hasattr(f, 'values') and len(f.values) == 0):
684 to_be_removed.append(f)
685 else:
686 del f.label
687 del f.description
688 del f.media
689 for f in to_be_removed:
690 c.delChild(f)
691 return c
693 class MultipleDataForm(DataForm):
694 def __init__(self, type_=None, title=None, instructions=None, items=None,
695 extend=None):
696 DataForm.__init__(self, type_=type_, title=title,
697 instructions=instructions, extend=extend)
698 # all records, recorded into DataRecords
699 if extend is None:
700 if items is not None:
701 self.items = items
702 else:
703 # we already have xmpp.Node inside - try to convert all
704 # fields into DataField objects
705 if items is None:
706 self.items = list(self.iterTags('item'))
707 else:
708 for item in self.getTags('item'):
709 self.delChild(item)
710 self.items = items
711 reported_tag = self.getTag('reported')
712 self.reported = DataRecord(extend=reported_tag)
714 @nested_property
715 def items():
717 A list of all records
719 def fget(self):
720 return list(self.iter_records())
722 def fset(self, records):
723 fdel(self)
724 for record in records:
725 if not isinstance(record, DataRecord):
726 DataRecord(extend=record)
727 self.addChild(node=record)
729 def fdel(self):
730 for record in self.getTags('item'):
731 self.delChild(record)
733 return locals()
735 def iter_records(self):
736 for record in self.getTags('item'):
737 yield record
739 # @nested_property
740 # def reported():
741 # """
742 # DataRecord that contains descriptions of fields in records
743 # """
744 # def fget(self):
745 # return self.getTag('reported')
746 # def fset(self, record):
747 # try:
748 # self.delChild('reported')
749 # except:
750 # pass
752 # record.setName('reported')
753 # self.addChild(node=record)
754 # return locals()