Package pyxmpp :: Package jabber :: Module dataforms
[hide private]

Source Code for Module pyxmpp.jabber.dataforms

  1  # 
  2  # (C) Copyright 2005-2006 Jacek Konieczny <jajcus@jajcus.net> 
  3  # 
  4  # This program is free software; you can redistribute it and/or modify 
  5  # it under the terms of the GNU Lesser General Public License Version 
  6  # 2.1 as published by the Free Software Foundation. 
  7  # 
  8  # This program is distributed in the hope that it will be useful, 
  9  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
 10  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
 11  # GNU Lesser General Public License for more details. 
 12  # 
 13  # You should have received a copy of the GNU Lesser General Public 
 14  # License along with this program; if not, write to the Free Software 
 15  # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. 
 16  # 
 17  """Jabber Data Forms support. 
 18   
 19  Normative reference: 
 20    - `JEP 4 <http://www.jabber.org/jeps/jep-0004.html>`__ 
 21  """ 
 22   
 23  __revision__="$Id: disco.py 513 2005-01-09 16:34:00Z jajcus $" 
 24  __docformat__="restructuredtext en" 
 25   
 26  import copy 
 27  import libxml2 
 28  import warnings 
 29  from pyxmpp.objects import StanzaPayloadObject 
 30  from pyxmpp.utils import from_utf8, to_utf8 
 31  from pyxmpp.xmlextra import xml_element_ns_iter 
 32  from pyxmpp.jid import JID 
 33  from pyxmpp.exceptions import BadRequestProtocolError 
 34   
 35  DATAFORM_NS = "jabber:x:data" 
36 37 -class Option(StanzaPayloadObject):
38 """One of optional data form field values. 39 40 :Ivariables: 41 - `label`: option label. 42 - `values`: option values. 43 :Types: 44 - `label`: `unicode` 45 - `values`: `list` of `unicode` 46 """ 47 xml_element_name = "option" 48 xml_element_namespace = DATAFORM_NS 49
50 - def __init__(self, value = None, label = None, values = None):
51 """Initialize an `Option` object. 52 53 :Parameters: 54 - `value`: option value (mandatory). 55 - `label`: option label (human-readable description). 56 - `values`: for backward compatibility only. 57 :Types: 58 - `label`: `unicode` 59 - `value`: `unicode` 60 """ 61 self.label = label 62 63 if value: 64 self.value = value 65 elif values: 66 warnings.warn("Option constructor accepts only single value now.", DeprecationWarning, stacklevel=1) 67 self.value = values[0] 68 else: 69 raise TypeError, "value argument to pyxmpp.dataforms.Option is required"
70 71 72 @property
73 - def values(self):
74 """Return list of option values (always single element). Obsolete. For 75 backwards compatibility only.""" 76 return [self.value]
77
78 - def complete_xml_element(self, xmlnode, doc):
79 """Complete the XML node with `self` content. 80 81 :Parameters: 82 - `xmlnode`: XML node with the element being built. It has already 83 right name and namespace, but no attributes or content. 84 - `doc`: document to which the element belongs. 85 :Types: 86 - `xmlnode`: `libxml2.xmlNode` 87 - `doc`: `libxml2.xmlDoc`""" 88 _unused = doc 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 - def _new_from_xml(cls, xmlnode):
94 """Create a new `Option` object from an XML element. 95 96 :Parameters: 97 - `xmlnode`: the XML element. 98 :Types: 99 - `xmlnode`: `libxml2.xmlNode` 100 101 :return: the object created. 102 :returntype: `Option` 103 """ 104 label = from_utf8(xmlnode.prop("label")) 105 child = xmlnode.children 106 value = None 107 for child in xml_element_ns_iter(xmlnode.children, DATAFORM_NS): 108 if child.name == "value": 109 value = from_utf8(child.getContent()) 110 break 111 if value is None: 112 raise BadRequestProtocolError, "No value in <option/> element" 113 return cls(value, label)
114 _new_from_xml = classmethod(_new_from_xml)
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
197 - def __getattr__(self, name):
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
225 - def __setattr__(self, name, value):
226 if name != "value": 227 self.__dict__[name] = value 228 return 229 if value is None: 230 self.values = [] 231 return 232 t = self.type 233 if t == "boolean": 234 if value: 235 self.values = ["1"] 236 else: 237 self.values = ["0"] 238 return 239 if t and t.endswith("-multi"): 240 values = list(value) 241 else: 242 values = [value] 243 if t and t.startswith("jid-"): 244 values = [JID(v).as_unicode() for v in values] 245 self.values = values
246
247 - def add_option(self, value, label):
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
266 - def complete_xml_element(self, xmlnode, doc):
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 xmlnode.setProp("type", self.type) 279 if not self.label is None: 280 xmlnode.setProp("label", self.label) 281 if not self.name is None: 282 xmlnode.setProp("var", self.name) 283 if self.values: 284 if self.type and len(self.values) > 1 and not self.type.endswith(u"-multi"): 285 raise ValueError, "Multiple values not allowed for %r field" % (self.type,) 286 for value in self.values: 287 xmlnode.newTextChild(xmlnode.ns(), "value", to_utf8(value)) 288 for option in self.options: 289 option.as_xml(xmlnode, doc) 290 if self.required: 291 xmlnode.newChild(xmlnode.ns(), "required", None) 292 if self.desc: 293 xmlnode.newTextChild(xmlnode.ns(), "desc", to_utf8(self.desc)) 294 return xmlnode
295
296 - def _new_from_xml(cls, xmlnode):
297 """Create a new `Field` object from an XML element. 298 299 :Parameters: 300 - `xmlnode`: the XML element. 301 :Types: 302 - `xmlnode`: `libxml2.xmlNode` 303 304 :return: the object created. 305 :returntype: `Field` 306 """ 307 field_type = xmlnode.prop("type") 308 label = from_utf8(xmlnode.prop("label")) 309 name = from_utf8(xmlnode.prop("var")) 310 child = xmlnode.children 311 values = [] 312 options = [] 313 required = False 314 desc = None 315 while child: 316 if child.type != "element" or child.ns().content != DATAFORM_NS: 317 pass 318 elif child.name == "required": 319 required = True 320 elif child.name == "desc": 321 desc = from_utf8(child.getContent()) 322 elif child.name == "value": 323 values.append(from_utf8(child.getContent())) 324 elif child.name == "option": 325 options.append(Option._new_from_xml(child)) 326 child = child.next 327 if field_type and not field_type.endswith("-multi") and len(values) > 1: 328 raise BadRequestProtocolError, "Multiple values for a single-value field" 329 return cls(name, values, field_type, label, options, required, desc)
330 _new_from_xml = classmethod(_new_from_xml)
331
332 -class Item(StanzaPayloadObject):
333 """An item of multi-item form data (e.g. a search result). 334 335 Additionally to the direct access to the contained fields via the `fields` attribute, 336 `Item` object provides an iterator and mapping interface for field access. E.g.:: 337 338 for field in item: 339 ... 340 341 or:: 342 343 field = item['field_name'] 344 345 or:: 346 347 if 'field_name' in item: 348 ... 349 350 :Ivariables: 351 - `fields`: the fields of the item. 352 :Types: 353 - `fields`: `list` of `Field`. 354 """ 355 xml_element_name = "item" 356 xml_element_namespace = DATAFORM_NS 357
358 - def __init__(self, fields = None):
359 """Initialize an `Item` object. 360 361 :Parameters: 362 - `fields`: item fields. 363 :Types: 364 - `fields`: `list` of `Field`. 365 """ 366 if fields is None: 367 self.fields = [] 368 else: 369 self.fields = list(fields)
370
371 - def __getitem__(self, name_or_index):
372 if isinstance(name_or_index, int): 373 return self.fields[name_or_index] 374 for f in self.fields: 375 if f.name == name_or_index: 376 return f 377 raise KeyError, name_or_index
378
379 - def __contains__(self, name):
380 for f in self.fields: 381 if f.name == name: 382 return True 383 return False
384
385 - def __iter__(self):
386 for field in self.fields: 387 yield field
388
389 - def add_field(self, name = None, values = None, field_type = None, 390 label = None, options = None, required = False, desc = None, value = None):
391 """Add a field to the item. 392 393 :Parameters: 394 - `name`: field name. 395 - `values`: raw field values. Not to be used together with `value`. 396 - `field_type`: field type. 397 - `label`: field label. 398 - `options`: optional values for the field. 399 - `required`: `True` if the field is required. 400 - `desc`: natural-language description of the field. 401 - `value`: field value or values in a field_type-specific type. May be used only 402 if `values` parameter is not provided. 403 :Types: 404 - `name`: `unicode` 405 - `values`: `list` of `unicode` 406 - `field_type`: `str` 407 - `label`: `unicode` 408 - `options`: `list` of `Option` 409 - `required`: `bool` 410 - `desc`: `unicode` 411 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 412 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 413 and `unicode` for other field types. 414 415 :return: the field added. 416 :returntype: `Field` 417 """ 418 field = Field(name, values, field_type, label, options, required, desc, value) 419 self.fields.append(field) 420 return field
421
422 - def complete_xml_element(self, xmlnode, doc):
423 """Complete the XML node with `self` content. 424 425 :Parameters: 426 - `xmlnode`: XML node with the element being built. It has already 427 right name and namespace, but no attributes or content. 428 - `doc`: document to which the element belongs. 429 :Types: 430 - `xmlnode`: `libxml2.xmlNode` 431 - `doc`: `libxml2.xmlDoc`""" 432 for field in self.fields: 433 field.as_xml(xmlnode, doc)
434
435 - def _new_from_xml(cls, xmlnode):
436 """Create a new `Item` object from an XML element. 437 438 :Parameters: 439 - `xmlnode`: the XML element. 440 :Types: 441 - `xmlnode`: `libxml2.xmlNode` 442 443 :return: the object created. 444 :returntype: `Item` 445 """ 446 child = xmlnode.children 447 fields = [] 448 while child: 449 if child.type != "element" or child.ns().content != DATAFORM_NS: 450 pass 451 elif child.name == "field": 452 fields.append(Field._new_from_xml(child)) 453 child = child.next 454 return cls(fields)
455 _new_from_xml = classmethod(_new_from_xml)
456
457 -class Form(StanzaPayloadObject):
458 """A JEP-0004 compliant data form. 459 460 Additionally to the direct access to the contained fields via the `fields` attribute, 461 `Form` object provides an iterator and mapping interface for field access. E.g.:: 462 463 for field in form: 464 ... 465 466 or:: 467 468 field = form['field_name'] 469 470 :Ivariables: 471 - `type`: form type ("form", "submit", "cancel" or "result"). 472 - `title`: form title. 473 - `instructions`: instructions for a form user. 474 - `fields`: the fields in the form. 475 - `reported_fields`: list of fields returned in a multi-item data form. 476 - `items`: items in a multi-item data form. 477 :Types: 478 - `title`: `unicode` 479 - `instructions`: `unicode` 480 - `fields`: `list` of `Field` 481 - `reported_fields`: `list` of `Field` 482 - `items`: `list` of `Item` 483 """ 484 allowed_types = ("form", "submit", "cancel", "result") 485 xml_element_name = "x" 486 xml_element_namespace = DATAFORM_NS 487
488 - def __init__(self, xmlnode_or_type = "form", title = None, instructions = None, 489 fields = None, reported_fields = None, items = None):
490 """Initialize a `Form` object. 491 492 :Parameters: 493 - `xmlnode_or_type`: XML element to parse or a form title. 494 - `title`: form title. 495 - `instructions`: instructions for the form. 496 - `fields`: form fields. 497 - `reported_fields`: fields reported in multi-item data. 498 - `items`: items of multi-item data. 499 :Types: 500 - `xmlnode_or_type`: `libxml2.xmlNode` or `str` 501 - `title`: `unicode` 502 - `instructions`: `unicode` 503 - `fields`: `list` of `Field` 504 - `reported_fields`: `list` of `Field` 505 - `items`: `list` of `Item` 506 """ 507 if isinstance(xmlnode_or_type, libxml2.xmlNode): 508 self.__from_xml(xmlnode_or_type) 509 elif xmlnode_or_type not in self.allowed_types: 510 raise ValueError, "Form type %r not allowed." % (xmlnode_or_type,) 511 else: 512 self.type = xmlnode_or_type 513 self.title = title 514 self.instructions = instructions 515 if fields: 516 self.fields = list(fields) 517 else: 518 self.fields = [] 519 if reported_fields: 520 self.reported_fields = list(reported_fields) 521 else: 522 self.reported_fields = [] 523 if items: 524 self.items = list(items) 525 else: 526 self.items = []
527
528 - def __getitem__(self, name_or_index):
529 if isinstance(name_or_index, int): 530 return self.fields[name_or_index] 531 for f in self.fields: 532 if f.name == name_or_index: 533 return f 534 raise KeyError, name_or_index
535
536 - def __contains__(self, name):
537 for f in self.fields: 538 if f.name == name: 539 return True 540 return False
541
542 - def __iter__(self):
543 for field in self.fields: 544 yield field
545
546 - def add_field(self, name = None, values = None, field_type = None, 547 label = None, options = None, required = False, desc = None, value = None):
548 """Add a field to the form. 549 550 :Parameters: 551 - `name`: field name. 552 - `values`: raw field values. Not to be used together with `value`. 553 - `field_type`: field type. 554 - `label`: field label. 555 - `options`: optional values for the field. 556 - `required`: `True` if the field is required. 557 - `desc`: natural-language description of the field. 558 - `value`: field value or values in a field_type-specific type. May be used only 559 if `values` parameter is not provided. 560 :Types: 561 - `name`: `unicode` 562 - `values`: `list` of `unicode` 563 - `field_type`: `str` 564 - `label`: `unicode` 565 - `options`: `list` of `Option` 566 - `required`: `bool` 567 - `desc`: `unicode` 568 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 569 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 570 and `unicode` for other field types. 571 572 :return: the field added. 573 :returntype: `Field` 574 """ 575 field = Field(name, values, field_type, label, options, required, desc, value) 576 self.fields.append(field) 577 return field
578
579 - def add_item(self, fields = None):
580 """Add and item to the form. 581 582 :Parameters: 583 - `fields`: fields of the item (they may be added later). 584 :Types: 585 - `fields`: `list` of `Field` 586 587 :return: the item added. 588 :returntype: `Item` 589 """ 590 item = Item(fields) 591 self.items.append(item) 592 return item
593
594 - def make_submit(self, keep_types = False):
595 """Make a "submit" form using data in `self`. 596 597 Remove uneeded information from the form. The information removed 598 includes: title, instructions, field labels, fixed fields etc. 599 600 :raise ValueError: when any required field has no value. 601 602 :Parameters: 603 - `keep_types`: when `True` field type information will be included 604 in the result form. That is usually not needed. 605 :Types: 606 - `keep_types`: `bool` 607 608 :return: the form created. 609 :returntype: `Form`""" 610 result = Form("submit") 611 for field in self.fields: 612 if field.type == "fixed": 613 continue 614 if not field.values: 615 if field.required: 616 raise ValueError, "Required field with no value!" 617 continue 618 if keep_types: 619 result.add_field(field.name, field.values, field.type) 620 else: 621 result.add_field(field.name, field.values) 622 return result
623
624 - def copy(self):
625 """Get a deep copy of `self`. 626 627 :return: a deep copy of `self`. 628 :returntype: `Form`""" 629 return copy.deepcopy(self)
630
631 - def complete_xml_element(self, xmlnode, doc):
632 """Complete the XML node with `self` content. 633 634 :Parameters: 635 - `xmlnode`: XML node with the element being built. It has already 636 right name and namespace, but no attributes or content. 637 - `doc`: document to which the element belongs. 638 :Types: 639 - `xmlnode`: `libxml2.xmlNode` 640 - `doc`: `libxml2.xmlDoc`""" 641 if self.type not in self.allowed_types: 642 raise ValueError, "Form type %r not allowed." % (self.type,) 643 xmlnode.setProp("type", self.type) 644 if self.type == "cancel": 645 return 646 ns = xmlnode.ns() 647 if self.title is not None: 648 xmlnode.newTextChild(ns, "title", self.title) 649 if self.instructions is not None: 650 xmlnode.newTextChild(ns, "instructions", self.instructions) 651 for field in self.fields: 652 field.as_xml(xmlnode, doc) 653 if self.type != "result": 654 return 655 if self.reported_fields: 656 reported = xmlnode.newChild(ns, "reported", None) 657 for field in self.reported_fields: 658 field.as_xml(reported, doc) 659 for item in self.items: 660 item.as_xml(xmlnode, doc)
661
662 - def __from_xml(self, xmlnode):
663 """Initialize a `Form` object from an XML element. 664 665 :Parameters: 666 - `xmlnode`: the XML element. 667 :Types: 668 - `xmlnode`: `libxml2.xmlNode` 669 """ 670 self.fields = [] 671 self.reported_fields = [] 672 self.items = [] 673 self.title = None 674 self.instructions = None 675 if (xmlnode.type != "element" or xmlnode.name != "x" 676 or xmlnode.ns().content != DATAFORM_NS): 677 raise ValueError, "Not a form: " + xmlnode.serialize() 678 self.type = xmlnode.prop("type") 679 if not self.type in self.allowed_types: 680 raise BadRequestProtocolError, "Bad form type: %r" % (self.type,) 681 child = xmlnode.children 682 while child: 683 if child.type != "element" or child.ns().content != DATAFORM_NS: 684 pass 685 elif child.name == "title": 686 self.title = from_utf8(child.getContent()) 687 elif child.name == "instructions": 688 self.instructions = from_utf8(child.getContent()) 689 elif child.name == "field": 690 self.fields.append(Field._new_from_xml(child)) 691 elif child.name == "item": 692 self.items.append(Item._new_from_xml(child)) 693 elif child.name == "reported": 694 self.__get_reported(child) 695 child = child.next
696
697 - def __get_reported(self, xmlnode):
698 """Parse the <reported/> element of the form. 699 700 :Parameters: 701 - `xmlnode`: the element to parse. 702 :Types: 703 - `xmlnode`: `libxml2.xmlNode`""" 704 child = xmlnode.children 705 while child: 706 if child.type != "element" or child.ns().content != DATAFORM_NS: 707 pass 708 elif child.name == "field": 709 self.reported_fields.append(Field._new_from_xml(child)) 710 child = child.next
711 # vi: sts=4 et sw=4 712