Reworked dataforms, not finished yet.
This commit is contained in:
		
							parent
							
								
									45cb666dfd
								
							
						
					
					
						commit
						289928050c
					
				
					 2 changed files with 358 additions and 0 deletions
				
			
		
							
								
								
									
										346
									
								
								src/common/dataforms-new.py
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								src/common/dataforms-new.py
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,346 @@ | ||||||
|  | # 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 Extends(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: 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(Extends, xmpp.Node): | ||||||
|  | 	""" 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(Extends, xmpp.Node): | ||||||
|  | 	'''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 = None | ||||||
|  | 		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 | ||||||
|  | 			if associated is not None: self.associated = associated | ||||||
|  | 		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(Extends, DataRecord): | ||||||
|  | 	def __init__(self, replyto=None, extends=None): | ||||||
|  | 		pass | ||||||
|  | 
 | ||||||
|  | 	@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 | ||||||
|  | 
 | ||||||
|  | 	@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) | ||||||
|  | 
 | ||||||
|  | class SimpleDataForm(DataForm): | ||||||
|  | 	pass | ||||||
|  | 
 | ||||||
|  | class MultipleDataForm(DataForm): | ||||||
|  | 	def __init__(self): | ||||||
|  | 		# all records, recorded into DataRecords | ||||||
|  | 
 | ||||||
|  | 	@nested_property | ||||||
|  | 	def records(): | ||||||
|  | 		''' A list of all records. ''' | ||||||
|  | 		 | ||||||
|  | @ -191,6 +191,18 @@ class Node: | ||||||
| 				else: nodes.append(node) | 				else: nodes.append(node) | ||||||
| 			if one and nodes: return nodes[0] | 			if one and nodes: return nodes[0] | ||||||
| 		if not one: return nodes | 		if not one: return nodes | ||||||
|  | 	 | ||||||
|  | 	def iterTags(self, name, attrs={}, namespace=None): | ||||||
|  | 		""" Iterate over all children using specified arguments as filter. """ | ||||||
|  | 		for node in self.kids: | ||||||
|  | 			if namespace is not None and namespace!=node.getNamespace(): continue | ||||||
|  | 			if node.getName() == name: | ||||||
|  | 				for key in attrs.keys(): | ||||||
|  | 					if not node.attrs.has_key(key) or \ | ||||||
|  | 						node.attrs[key]!=attrs[key]: break | ||||||
|  | 					else: | ||||||
|  | 						yield node | ||||||
|  | 
 | ||||||
| 	def setAttr(self, key, val): | 	def setAttr(self, key, val): | ||||||
| 		""" Sets attribute "key" with the value "val". """ | 		""" Sets attribute "key" with the value "val". """ | ||||||
| 		self.attrs[key]=val | 		self.attrs[key]=val | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue