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

Source Code for Module pyxmpp.jabber.dataforms

  1  # 
  2  # (C) Copyright 2005-2010 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 - `value`: option value. 43 :Types: 44 - `label`: `unicode` 45 - `value`: `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 if self.label is not None: 90 xmlnode.setProp("label", self.label.encode("utf-8")) 91 xmlnode.newTextChild(xmlnode.ns(), "value", self.value.encode("utf-8")) 92 return xmlnode
93 94 @classmethod
95 - def _new_from_xml(cls, xmlnode):
96 """Create a new `Option` object from an XML element. 97 98 :Parameters: 99 - `xmlnode`: the XML element. 100 :Types: 101 - `xmlnode`: `libxml2.xmlNode` 102 103 :return: the object created. 104 :returntype: `Option` 105 """ 106 label = from_utf8(xmlnode.prop("label")) 107 child = xmlnode.children 108 value = None 109 for child in xml_element_ns_iter(xmlnode.children, DATAFORM_NS): 110 if child.name == "value": 111 value = from_utf8(child.getContent()) 112 break 113 if value is None: 114 raise BadRequestProtocolError, "No value in <option/> element" 115 return cls(value, label)
116
117 -class Field(StanzaPayloadObject):
118 """A data form field. 119 120 :Ivariables: 121 - `name`: field name. 122 - `values`: field values. 123 - `value`: field value parsed according to the form type. 124 - `label`: field label (human-readable description). 125 - `type`: field type ("boolean", "fixed", "hidden", "jid-multi", 126 "jid-single", "list-multi", "list-single", "text-multi", 127 "text-private" or "text-single"). 128 - `options`: field options (for "list-multi" or "list-single" fields). 129 - `required`: `True` when the field is required. 130 - `desc`: natural-language description of the field. 131 :Types: 132 - `name`: `unicode` 133 - `values`: `list` of `unicode` 134 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 135 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 136 and `unicode` for other field types. 137 - `label`: `unicode` 138 - `type`: `str` 139 - `options`: `Option` 140 - `required`: `boolean` 141 - `desc`: `unicode` 142 """ 143 xml_element_name = "field" 144 xml_element_namespace = DATAFORM_NS 145 allowed_types = ("boolean", "fixed", "hidden", "jid-multi", 146 "jid-single", "list-multi", "list-single", "text-multi", 147 "text-private", "text-single")
148 - def __init__(self, name = None, values = None, field_type = None, label = None, 149 options = None, required = False, desc = None, value = None):
150 """Initialize a `Field` object. 151 152 :Parameters: 153 - `name`: field name. 154 - `values`: raw field values. Not to be used together with `value`. 155 - `field_type`: field type. 156 - `label`: field label. 157 - `options`: optional values for the field. 158 - `required`: `True` if the field is required. 159 - `desc`: natural-language description of the field. 160 - `value`: field value or values in a field_type-specific type. May be used only 161 if `values` parameter is not provided. 162 :Types: 163 - `name`: `unicode` 164 - `values`: `list` of `unicode` 165 - `field_type`: `str` 166 - `label`: `unicode` 167 - `options`: `list` of `Option` 168 - `required`: `bool` 169 - `desc`: `unicode` 170 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 171 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 172 and `unicode` for other field types. 173 """ 174 self.name = name 175 if field_type is not None and field_type not in self.allowed_types: 176 raise ValueError, "Invalid form field type: %r" % (field_type,) 177 self.type = field_type 178 if value is not None: 179 if values: 180 raise ValueError, "values or value must be given, not both" 181 self.value = value 182 elif not values: 183 self.values = [] 184 else: 185 self.values = list(values) 186 if field_type and not field_type.endswith("-multi") and len(self.values) > 1: 187 raise ValueError, "Multiple values for a single-value field" 188 self.label = label 189 if not options: 190 self.options = [] 191 elif field_type and not field_type.startswith("list-"): 192 raise ValueError, "Options not allowed for non-list fields" 193 else: 194 self.options = list(options) 195 self.required = required 196 self.desc = desc
197
198 - def __getattr__(self, name):
199 if name != "value": 200 raise AttributeError, "'Field' object has no attribute %r" % (name,) 201 values = self.values 202 t = self.type 203 l = len(values) 204 if t is not None: 205 if t == "boolean": 206 if l == 0: 207 return None 208 elif l == 1: 209 v = values[0] 210 if v in ("0","false"): 211 return False 212 elif v in ("1","true"): 213 return True 214 raise ValueError, "Bad boolean value" 215 elif t.startswith("jid-"): 216 values = [JID(v) for v in values] 217 if t.endswith("-multi"): 218 return values 219 if l == 0: 220 return None 221 elif l == 1: 222 return values[0] 223 else: 224 raise ValueError, "Multiple values of a single-value field"
225
226 - def __setattr__(self, name, value):
227 if name != "value": 228 self.__dict__[name] = value 229 return 230 if value is None: 231 self.values = [] 232 return 233 t = self.type 234 if t == "boolean": 235 if value: 236 self.values = ["1"] 237 else: 238 self.values = ["0"] 239 return 240 if t and t.endswith("-multi"): 241 values = list(value) 242 else: 243 values = [value] 244 if t and t.startswith("jid-"): 245 values = [JID(v).as_unicode() for v in values] 246 self.values = values
247
248 - def add_option(self, value, label):
249 """Add an option for the field. 250 251 :Parameters: 252 - `value`: option values. 253 - `label`: option label (human-readable description). 254 :Types: 255 - `value`: `list` of `unicode` 256 - `label`: `unicode` 257 """ 258 if type(value) is list: 259 warnings.warn(".add_option() accepts single value now.", DeprecationWarning, stacklevel=1) 260 value = value[0] 261 if self.type not in ("list-multi", "list-single"): 262 raise ValueError, "Options are allowed only for list types." 263 option = Option(value, label) 264 self.options.append(option) 265 return option
266
267 - def complete_xml_element(self, xmlnode, doc):
268 """Complete the XML node with `self` content. 269 270 :Parameters: 271 - `xmlnode`: XML node with the element being built. It has already 272 right name and namespace, but no attributes or content. 273 - `doc`: document to which the element belongs. 274 :Types: 275 - `xmlnode`: `libxml2.xmlNode` 276 - `doc`: `libxml2.xmlDoc`""" 277 if self.type is not None and self.type not in self.allowed_types: 278 raise ValueError, "Invalid form field type: %r" % (self.type,) 279 if self.type is not None: 280 xmlnode.setProp("type", self.type) 281 if not self.label is None: 282 xmlnode.setProp("label", to_utf8(self.label)) 283 if not self.name is None: 284 xmlnode.setProp("var", to_utf8(self.name)) 285 if self.values: 286 if self.type and len(self.values) > 1 and not self.type.endswith(u"-multi"): 287 raise ValueError, "Multiple values not allowed for %r field" % (self.type,) 288 for value in self.values: 289 xmlnode.newTextChild(xmlnode.ns(), "value", to_utf8(value)) 290 for option in self.options: 291 option.as_xml(xmlnode, doc) 292 if self.required: 293 xmlnode.newChild(xmlnode.ns(), "required", None) 294 if self.desc: 295 xmlnode.newTextChild(xmlnode.ns(), "desc", to_utf8(self.desc)) 296 return xmlnode
297 298 @classmethod
299 - def _new_from_xml(cls, xmlnode):
300 """Create a new `Field` object from an XML element. 301 302 :Parameters: 303 - `xmlnode`: the XML element. 304 :Types: 305 - `xmlnode`: `libxml2.xmlNode` 306 307 :return: the object created. 308 :returntype: `Field` 309 """ 310 field_type = xmlnode.prop("type") 311 label = from_utf8(xmlnode.prop("label")) 312 name = from_utf8(xmlnode.prop("var")) 313 child = xmlnode.children 314 values = [] 315 options = [] 316 required = False 317 desc = None 318 while child: 319 if child.type != "element" or child.ns().content != DATAFORM_NS: 320 pass 321 elif child.name == "required": 322 required = True 323 elif child.name == "desc": 324 desc = from_utf8(child.getContent()) 325 elif child.name == "value": 326 values.append(from_utf8(child.getContent())) 327 elif child.name == "option": 328 options.append(Option._new_from_xml(child)) 329 child = child.next 330 if field_type and not field_type.endswith("-multi") and len(values) > 1: 331 raise BadRequestProtocolError, "Multiple values for a single-value field" 332 return cls(name, values, field_type, label, options, required, desc)
333
334 -class Item(StanzaPayloadObject):
335 """An item of multi-item form data (e.g. a search result). 336 337 Additionally to the direct access to the contained fields via the `fields` attribute, 338 `Item` object provides an iterator and mapping interface for field access. E.g.:: 339 340 for field in item: 341 ... 342 343 or:: 344 345 field = item['field_name'] 346 347 or:: 348 349 if 'field_name' in item: 350 ... 351 352 :Ivariables: 353 - `fields`: the fields of the item. 354 :Types: 355 - `fields`: `list` of `Field`. 356 """ 357 xml_element_name = "item" 358 xml_element_namespace = DATAFORM_NS 359
360 - def __init__(self, fields = None):
361 """Initialize an `Item` object. 362 363 :Parameters: 364 - `fields`: item fields. 365 :Types: 366 - `fields`: `list` of `Field`. 367 """ 368 if fields is None: 369 self.fields = [] 370 else: 371 self.fields = list(fields)
372
373 - def __getitem__(self, name_or_index):
374 if isinstance(name_or_index, int): 375 return self.fields[name_or_index] 376 for f in self.fields: 377 if f.name == name_or_index: 378 return f 379 raise KeyError, name_or_index
380
381 - def __contains__(self, name):
382 for f in self.fields: 383 if f.name == name: 384 return True 385 return False
386
387 - def __iter__(self):
388 for field in self.fields: 389 yield field
390
391 - def add_field(self, name = None, values = None, field_type = None, 392 label = None, options = None, required = False, desc = None, value = None):
393 """Add a field to the item. 394 395 :Parameters: 396 - `name`: field name. 397 - `values`: raw field values. Not to be used together with `value`. 398 - `field_type`: field type. 399 - `label`: field label. 400 - `options`: optional values for the field. 401 - `required`: `True` if the field is required. 402 - `desc`: natural-language description of the field. 403 - `value`: field value or values in a field_type-specific type. May be used only 404 if `values` parameter is not provided. 405 :Types: 406 - `name`: `unicode` 407 - `values`: `list` of `unicode` 408 - `field_type`: `str` 409 - `label`: `unicode` 410 - `options`: `list` of `Option` 411 - `required`: `bool` 412 - `desc`: `unicode` 413 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 414 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 415 and `unicode` for other field types. 416 417 :return: the field added. 418 :returntype: `Field` 419 """ 420 field = Field(name, values, field_type, label, options, required, desc, value) 421 self.fields.append(field) 422 return field
423
424 - def complete_xml_element(self, xmlnode, doc):
425 """Complete the XML node with `self` content. 426 427 :Parameters: 428 - `xmlnode`: XML node with the element being built. It has already 429 right name and namespace, but no attributes or content. 430 - `doc`: document to which the element belongs. 431 :Types: 432 - `xmlnode`: `libxml2.xmlNode` 433 - `doc`: `libxml2.xmlDoc`""" 434 for field in self.fields: 435 field.as_xml(xmlnode, doc)
436 437 @classmethod
438 - def _new_from_xml(cls, xmlnode):
439 """Create a new `Item` object from an XML element. 440 441 :Parameters: 442 - `xmlnode`: the XML element. 443 :Types: 444 - `xmlnode`: `libxml2.xmlNode` 445 446 :return: the object created. 447 :returntype: `Item` 448 """ 449 child = xmlnode.children 450 fields = [] 451 while child: 452 if child.type != "element" or child.ns().content != DATAFORM_NS: 453 pass 454 elif child.name == "field": 455 fields.append(Field._new_from_xml(child)) 456 child = child.next 457 return cls(fields)
458
459 -class Form(StanzaPayloadObject):
460 """A JEP-0004 compliant data form. 461 462 Additionally to the direct access to the contained fields via the `fields` attribute, 463 `Form` object provides an iterator and mapping interface for field access. E.g.:: 464 465 for field in form: 466 ... 467 468 or:: 469 470 field = form['field_name'] 471 472 :Ivariables: 473 - `type`: form type ("form", "submit", "cancel" or "result"). 474 - `title`: form title. 475 - `instructions`: instructions for a form user. 476 - `fields`: the fields in the form. 477 - `reported_fields`: list of fields returned in a multi-item data form. 478 - `items`: items in a multi-item data form. 479 :Types: 480 - `title`: `unicode` 481 - `instructions`: `unicode` 482 - `fields`: `list` of `Field` 483 - `reported_fields`: `list` of `Field` 484 - `items`: `list` of `Item` 485 """ 486 allowed_types = ("form", "submit", "cancel", "result") 487 xml_element_name = "x" 488 xml_element_namespace = DATAFORM_NS 489
490 - def __init__(self, xmlnode_or_type = "form", title = None, instructions = None, 491 fields = None, reported_fields = None, items = None):
492 """Initialize a `Form` object. 493 494 :Parameters: 495 - `xmlnode_or_type`: XML element to parse or a form title. 496 - `title`: form title. 497 - `instructions`: instructions for the form. 498 - `fields`: form fields. 499 - `reported_fields`: fields reported in multi-item data. 500 - `items`: items of multi-item data. 501 :Types: 502 - `xmlnode_or_type`: `libxml2.xmlNode` or `str` 503 - `title`: `unicode` 504 - `instructions`: `unicode` 505 - `fields`: `list` of `Field` 506 - `reported_fields`: `list` of `Field` 507 - `items`: `list` of `Item` 508 """ 509 if isinstance(xmlnode_or_type, libxml2.xmlNode): 510 self.__from_xml(xmlnode_or_type) 511 elif xmlnode_or_type not in self.allowed_types: 512 raise ValueError, "Form type %r not allowed." % (xmlnode_or_type,) 513 else: 514 self.type = xmlnode_or_type 515 self.title = title 516 self.instructions = instructions 517 if fields: 518 self.fields = list(fields) 519 else: 520 self.fields = [] 521 if reported_fields: 522 self.reported_fields = list(reported_fields) 523 else: 524 self.reported_fields = [] 525 if items: 526 self.items = list(items) 527 else: 528 self.items = []
529
530 - def __getitem__(self, name_or_index):
531 if isinstance(name_or_index, int): 532 return self.fields[name_or_index] 533 for f in self.fields: 534 if f.name == name_or_index: 535 return f 536 raise KeyError, name_or_index
537
538 - def __contains__(self, name):
539 for f in self.fields: 540 if f.name == name: 541 return True 542 return False
543
544 - def __iter__(self):
545 for field in self.fields: 546 yield field
547
548 - def add_field(self, name = None, values = None, field_type = None, 549 label = None, options = None, required = False, desc = None, value = None):
550 """Add a field to the form. 551 552 :Parameters: 553 - `name`: field name. 554 - `values`: raw field values. Not to be used together with `value`. 555 - `field_type`: field type. 556 - `label`: field label. 557 - `options`: optional values for the field. 558 - `required`: `True` if the field is required. 559 - `desc`: natural-language description of the field. 560 - `value`: field value or values in a field_type-specific type. May be used only 561 if `values` parameter is not provided. 562 :Types: 563 - `name`: `unicode` 564 - `values`: `list` of `unicode` 565 - `field_type`: `str` 566 - `label`: `unicode` 567 - `options`: `list` of `Option` 568 - `required`: `bool` 569 - `desc`: `unicode` 570 - `value`: `bool` for "boolean" field, `JID` for "jid-single", `list` of `JID` 571 for "jid-multi", `list` of `unicode` for "list-multi" and "text-multi" 572 and `unicode` for other field types. 573 574 :return: the field added. 575 :returntype: `Field` 576 """ 577 field = Field(name, values, field_type, label, options, required, desc, value) 578 self.fields.append(field) 579 return field
580
581 - def add_item(self, fields = None):
582 """Add and item to the form. 583 584 :Parameters: 585 - `fields`: fields of the item (they may be added later). 586 :Types: 587 - `fields`: `list` of `Field` 588 589 :return: the item added. 590 :returntype: `Item` 591 """ 592 item = Item(fields) 593 self.items.append(item) 594 return item
595
596 - def make_submit(self, keep_types = False):
597 """Make a "submit" form using data in `self`. 598 599 Remove uneeded information from the form. The information removed 600 includes: title, instructions, field labels, fixed fields etc. 601 602 :raise ValueError: when any required field has no value. 603 604 :Parameters: 605 - `keep_types`: when `True` field type information will be included 606 in the result form. That is usually not needed. 607 :Types: 608 - `keep_types`: `bool` 609 610 :return: the form created. 611 :returntype: `Form`""" 612 result = Form("submit") 613 for field in self.fields: 614 if field.type == "fixed": 615 continue 616 if not field.values: 617 if field.required: 618 raise ValueError, "Required field with no value!" 619 continue 620 if keep_types: 621 result.add_field(field.name, field.values, field.type) 622 else: 623 result.add_field(field.name, field.values) 624 return result
625
626 - def copy(self):
627 """Get a deep copy of `self`. 628 629 :return: a deep copy of `self`. 630 :returntype: `Form`""" 631 return copy.deepcopy(self)
632
633 - def complete_xml_element(self, xmlnode, doc):
634 """Complete the XML node with `self` content. 635 636 :Parameters: 637 - `xmlnode`: XML node with the element being built. It has already 638 right name and namespace, but no attributes or content. 639 - `doc`: document to which the element belongs. 640 :Types: 641 - `xmlnode`: `libxml2.xmlNode` 642 - `doc`: `libxml2.xmlDoc`""" 643 if self.type not in self.allowed_types: 644 raise ValueError, "Form type %r not allowed." % (self.type,) 645 xmlnode.setProp("type", self.type) 646 if self.type == "cancel": 647 return 648 ns = xmlnode.ns() 649 if self.title is not None: 650 xmlnode.newTextChild(ns, "title", to_utf8(self.title)) 651 if self.instructions is not None: 652 xmlnode.newTextChild(ns, "instructions", to_utf8(self.instructions)) 653 for field in self.fields: 654 field.as_xml(xmlnode, doc) 655 if self.type != "result": 656 return 657 if self.reported_fields: 658 reported = xmlnode.newChild(ns, "reported", None) 659 for field in self.reported_fields: 660 field.as_xml(reported, doc) 661 for item in self.items: 662 item.as_xml(xmlnode, doc)
663
664 - def __from_xml(self, xmlnode):
665 """Initialize a `Form` object from an XML element. 666 667 :Parameters: 668 - `xmlnode`: the XML element. 669 :Types: 670 - `xmlnode`: `libxml2.xmlNode` 671 """ 672 self.fields = [] 673 self.reported_fields = [] 674 self.items = [] 675 self.title = None 676 self.instructions = None 677 if (xmlnode.type != "element" or xmlnode.name != "x" 678 or xmlnode.ns().content != DATAFORM_NS): 679 raise ValueError, "Not a form: " + xmlnode.serialize() 680 self.type = xmlnode.prop("type") 681 if not self.type in self.allowed_types: 682 raise BadRequestProtocolError, "Bad form type: %r" % (self.type,) 683 child = xmlnode.children 684 while child: 685 if child.type != "element" or child.ns().content != DATAFORM_NS: 686 pass 687 elif child.name == "title": 688 self.title = from_utf8(child.getContent()) 689 elif child.name == "instructions": 690 self.instructions = from_utf8(child.getContent()) 691 elif child.name == "field": 692 self.fields.append(Field._new_from_xml(child)) 693 elif child.name == "item": 694 self.items.append(Item._new_from_xml(child)) 695 elif child.name == "reported": 696 self.__get_reported(child) 697 child = child.next
698
699 - def __get_reported(self, xmlnode):
700 """Parse the <reported/> element of the form. 701 702 :Parameters: 703 - `xmlnode`: the element to parse. 704 :Types: 705 - `xmlnode`: `libxml2.xmlNode`""" 706 child = xmlnode.children 707 while child: 708 if child.type != "element" or child.ns().content != DATAFORM_NS: 709 pass 710 elif child.name == "field": 711 self.reported_fields.append(Field._new_from_xml(child)) 712 child = child.next
713 # vi: sts=4 et sw=4 714