1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 """Jabber Data Forms support.
18
19 Normative reference:
20 - `JEP 4 <http://www.jabber.org/jeps/jep-0004.html>`__
21 """
22
23 __docformat__="restructuredtext en"
24
25 import copy
26 import libxml2
27 import warnings
28 from pyxmpp.objects import StanzaPayloadObject
29 from pyxmpp.utils import from_utf8, to_utf8
30 from pyxmpp.xmlextra import xml_element_ns_iter
31 from pyxmpp.jid import JID
32 from pyxmpp.exceptions import BadRequestProtocolError
33
34 DATAFORM_NS = "jabber:x:data"
35
36 -class Option(StanzaPayloadObject):
37 """One of optional data form field values.
38
39 :Ivariables:
40 - `label`: option label.
41 - `value`: option value.
42 :Types:
43 - `label`: `unicode`
44 - `value`: `unicode`
45 """
46 xml_element_name = "option"
47 xml_element_namespace = DATAFORM_NS
48
49 - def __init__(self, value = None, label = None, values = None):
50 """Initialize an `Option` object.
51
52 :Parameters:
53 - `value`: option value (mandatory).
54 - `label`: option label (human-readable description).
55 - `values`: for backward compatibility only.
56 :Types:
57 - `label`: `unicode`
58 - `value`: `unicode`
59 """
60 self.label = label
61
62 if value:
63 self.value = value
64 elif values:
65 warnings.warn("Option constructor accepts only single value now.", DeprecationWarning, stacklevel=1)
66 self.value = values[0]
67 else:
68 raise TypeError, "value argument to pyxmpp.dataforms.Option is required"
69
70
71 @property
73 """Return list of option values (always single element). Obsolete. For
74 backwards compatibility only."""
75 return [self.value]
76
78 """Complete the XML node with `self` content.
79
80 :Parameters:
81 - `xmlnode`: XML node with the element being built. It has already
82 right name and namespace, but no attributes or content.
83 - `doc`: document to which the element belongs.
84 :Types:
85 - `xmlnode`: `libxml2.xmlNode`
86 - `doc`: `libxml2.xmlDoc`"""
87 _unused = doc
88 if self.label is not None:
89 xmlnode.setProp("label", self.label.encode("utf-8"))
90 xmlnode.newTextChild(xmlnode.ns(), "value", self.value.encode("utf-8"))
91 return xmlnode
92
93 @classmethod
95 """Create a new `Option` object from an XML element.
96
97 :Parameters:
98 - `xmlnode`: the XML element.
99 :Types:
100 - `xmlnode`: `libxml2.xmlNode`
101
102 :return: the object created.
103 :returntype: `Option`
104 """
105 label = from_utf8(xmlnode.prop("label"))
106 child = xmlnode.children
107 value = None
108 for child in xml_element_ns_iter(xmlnode.children, DATAFORM_NS):
109 if child.name == "value":
110 value = from_utf8(child.getContent())
111 break
112 if value is None:
113 raise BadRequestProtocolError, "No value in <option/> element"
114 return cls(value, label)
115
116 -class Field(StanzaPayloadObject):
117 """A data form field.
118
119 :Ivariables:
120 - `name`: field name.
121 - `values`: field values.
122 - `value`: field value parsed according to the form type.
123 - `label`: field label (human-readable description).
124 - `type`: field type ("boolean", "fixed", "hidden", "jid-multi",
125 "jid-single", "list-multi", "list-single", "text-multi",
126 "text-private" or "text-single").
127 - `options`: field options (for "list-multi" or "list-single" fields).
128 - `required`: `True` when the field is required.
129 - `desc`: natural-language description of the field.
130 :Types:
131 - `name`: `unicode`
132 - `values`: `list` of `unicode`
133 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID`
134 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi"
135 and `unicode` for other field types.
136 - `label`: `unicode`
137 - `type`: `str`
138 - `options`: `Option`
139 - `required`: `boolean`
140 - `desc`: `unicode`
141 """
142 xml_element_name = "field"
143 xml_element_namespace = DATAFORM_NS
144 allowed_types = ("boolean", "fixed", "hidden", "jid-multi",
145 "jid-single", "list-multi", "list-single", "text-multi",
146 "text-private", "text-single")
147 - def __init__(self, name = None, values = None, field_type = None, label = None,
148 options = None, required = False, desc = None, value = None):
149 """Initialize a `Field` object.
150
151 :Parameters:
152 - `name`: field name.
153 - `values`: raw field values. Not to be used together with `value`.
154 - `field_type`: field type.
155 - `label`: field label.
156 - `options`: optional values for the field.
157 - `required`: `True` if the field is required.
158 - `desc`: natural-language description of the field.
159 - `value`: field value or values in a field_type-specific type. May be used only
160 if `values` parameter is not provided.
161 :Types:
162 - `name`: `unicode`
163 - `values`: `list` of `unicode`
164 - `field_type`: `str`
165 - `label`: `unicode`
166 - `options`: `list` of `Option`
167 - `required`: `bool`
168 - `desc`: `unicode`
169 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID`
170 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi"
171 and `unicode` for other field types.
172 """
173 self.name = name
174 if field_type is not None and field_type not in self.allowed_types:
175 raise ValueError, "Invalid form field type: %r" % (field_type,)
176 self.type = field_type
177 if value is not None:
178 if values:
179 raise ValueError, "values or value must be given, not both"
180 self.value = value
181 elif not values:
182 self.values = []
183 else:
184 self.values = list(values)
185 if field_type and not field_type.endswith("-multi") and len(self.values) > 1:
186 raise ValueError, "Multiple values for a single-value field"
187 self.label = label
188 if not options:
189 self.options = []
190 elif field_type and not field_type.startswith("list-"):
191 raise ValueError, "Options not allowed for non-list fields"
192 else:
193 self.options = list(options)
194 self.required = required
195 self.desc = desc
196
198 if name != "value":
199 raise AttributeError, "'Field' object has no attribute %r" % (name,)
200 values = self.values
201 t = self.type
202 l = len(values)
203 if t is not None:
204 if t == "boolean":
205 if l == 0:
206 return None
207 elif l == 1:
208 v = values[0]
209 if v in ("0","false"):
210 return False
211 elif v in ("1","true"):
212 return True
213 raise ValueError, "Bad boolean value"
214 elif t.startswith("jid-"):
215 values = [JID(v) for v in values]
216 if t.endswith("-multi"):
217 return values
218 if l == 0:
219 return None
220 elif l == 1:
221 return values[0]
222 else:
223 raise ValueError, "Multiple values of a single-value field"
224
246
248 """Add an option for the field.
249
250 :Parameters:
251 - `value`: option values.
252 - `label`: option label (human-readable description).
253 :Types:
254 - `value`: `list` of `unicode`
255 - `label`: `unicode`
256 """
257 if type(value) is list:
258 warnings.warn(".add_option() accepts single value now.", DeprecationWarning, stacklevel=1)
259 value = value[0]
260 if self.type not in ("list-multi", "list-single"):
261 raise ValueError, "Options are allowed only for list types."
262 option = Option(value, label)
263 self.options.append(option)
264 return option
265
267 """Complete the XML node with `self` content.
268
269 :Parameters:
270 - `xmlnode`: XML node with the element being built. It has already
271 right name and namespace, but no attributes or content.
272 - `doc`: document to which the element belongs.
273 :Types:
274 - `xmlnode`: `libxml2.xmlNode`
275 - `doc`: `libxml2.xmlDoc`"""
276 if self.type is not None and self.type not in self.allowed_types:
277 raise ValueError, "Invalid form field type: %r" % (self.type,)
278 if self.type is not None:
279 xmlnode.setProp("type", self.type)
280 if not self.label is None:
281 xmlnode.setProp("label", to_utf8(self.label))
282 if not self.name is None:
283 xmlnode.setProp("var", to_utf8(self.name))
284 if self.values:
285 if self.type and len(self.values) > 1 and not self.type.endswith(u"-multi"):
286 raise ValueError, "Multiple values not allowed for %r field" % (self.type,)
287 for value in self.values:
288 xmlnode.newTextChild(xmlnode.ns(), "value", to_utf8(value))
289 for option in self.options:
290 option.as_xml(xmlnode, doc)
291 if self.required:
292 xmlnode.newChild(xmlnode.ns(), "required", None)
293 if self.desc:
294 xmlnode.newTextChild(xmlnode.ns(), "desc", to_utf8(self.desc))
295 return xmlnode
296
297 @classmethod
299 """Create a new `Field` object from an XML element.
300
301 :Parameters:
302 - `xmlnode`: the XML element.
303 :Types:
304 - `xmlnode`: `libxml2.xmlNode`
305
306 :return: the object created.
307 :returntype: `Field`
308 """
309 field_type = xmlnode.prop("type")
310 label = from_utf8(xmlnode.prop("label"))
311 name = from_utf8(xmlnode.prop("var"))
312 child = xmlnode.children
313 values = []
314 options = []
315 required = False
316 desc = None
317 while child:
318 if child.type != "element" or child.ns().content != DATAFORM_NS:
319 pass
320 elif child.name == "required":
321 required = True
322 elif child.name == "desc":
323 desc = from_utf8(child.getContent())
324 elif child.name == "value":
325 values.append(from_utf8(child.getContent()))
326 elif child.name == "option":
327 options.append(Option._new_from_xml(child))
328 child = child.next
329 if field_type and not field_type.endswith("-multi") and len(values) > 1:
330 raise BadRequestProtocolError, "Multiple values for a single-value field"
331 return cls(name, values, field_type, label, options, required, desc)
332
333 -class Item(StanzaPayloadObject):
334 """An item of multi-item form data (e.g. a search result).
335
336 Additionally to the direct access to the contained fields via the `fields` attribute,
337 `Item` object provides an iterator and mapping interface for field access. E.g.::
338
339 for field in item:
340 ...
341
342 or::
343
344 field = item['field_name']
345
346 or::
347
348 if 'field_name' in item:
349 ...
350
351 :Ivariables:
352 - `fields`: the fields of the item.
353 :Types:
354 - `fields`: `list` of `Field`.
355 """
356 xml_element_name = "item"
357 xml_element_namespace = DATAFORM_NS
358
360 """Initialize an `Item` object.
361
362 :Parameters:
363 - `fields`: item fields.
364 :Types:
365 - `fields`: `list` of `Field`.
366 """
367 if fields is None:
368 self.fields = []
369 else:
370 self.fields = list(fields)
371
373 if isinstance(name_or_index, int):
374 return self.fields[name_or_index]
375 for f in self.fields:
376 if f.name == name_or_index:
377 return f
378 raise KeyError, name_or_index
379
381 for f in self.fields:
382 if f.name == name:
383 return True
384 return False
385
387 for field in self.fields:
388 yield field
389
390 - def add_field(self, name = None, values = None, field_type = None,
391 label = None, options = None, required = False, desc = None, value = None):
392 """Add a field to the item.
393
394 :Parameters:
395 - `name`: field name.
396 - `values`: raw field values. Not to be used together with `value`.
397 - `field_type`: field type.
398 - `label`: field label.
399 - `options`: optional values for the field.
400 - `required`: `True` if the field is required.
401 - `desc`: natural-language description of the field.
402 - `value`: field value or values in a field_type-specific type. May be used only
403 if `values` parameter is not provided.
404 :Types:
405 - `name`: `unicode`
406 - `values`: `list` of `unicode`
407 - `field_type`: `str`
408 - `label`: `unicode`
409 - `options`: `list` of `Option`
410 - `required`: `bool`
411 - `desc`: `unicode`
412 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID`
413 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi"
414 and `unicode` for other field types.
415
416 :return: the field added.
417 :returntype: `Field`
418 """
419 field = Field(name, values, field_type, label, options, required, desc, value)
420 self.fields.append(field)
421 return field
422
424 """Complete the XML node with `self` content.
425
426 :Parameters:
427 - `xmlnode`: XML node with the element being built. It has already
428 right name and namespace, but no attributes or content.
429 - `doc`: document to which the element belongs.
430 :Types:
431 - `xmlnode`: `libxml2.xmlNode`
432 - `doc`: `libxml2.xmlDoc`"""
433 for field in self.fields:
434 field.as_xml(xmlnode, doc)
435
436 @classmethod
438 """Create a new `Item` object from an XML element.
439
440 :Parameters:
441 - `xmlnode`: the XML element.
442 :Types:
443 - `xmlnode`: `libxml2.xmlNode`
444
445 :return: the object created.
446 :returntype: `Item`
447 """
448 child = xmlnode.children
449 fields = []
450 while child:
451 if child.type != "element" or child.ns().content != DATAFORM_NS:
452 pass
453 elif child.name == "field":
454 fields.append(Field._new_from_xml(child))
455 child = child.next
456 return cls(fields)
457
712
713