Package qm :: Module fields
[hide private]
[frames] | no frames]

Source Code for Module qm.fields

   1  ######################################################################## 
   2  # 
   3  # File:   fields.py 
   4  # Author: Alex Samuel 
   5  # Date:   2001-03-05 
   6  # 
   7  # Contents: 
   8  #   General type system for user-defined data constructs. 
   9  # 
  10  # Copyright (c) 2001, 2002, 2003 by CodeSourcery, LLC.  All rights reserved.  
  11  # 
  12  # For license terms see the file COPYING. 
  13  # 
  14  ######################################################################## 
  15   
  16  """A 'Field' determines how data is displayed and stored. 
  17   
  18  A 'Field' is a component of a data structure.  Every 'Field' has a type. 
  19  For example, an 'IntegerField' stores a signed integer while a 
  20  'TextField' stores a string. 
  21   
  22  The value of a 'Field' can be represented as HTML (for display in the 
  23  GUI), or as XML (when written to persistent storage).  Every 'Field' can 
  24  create an HTML form that can be used by the user to update the value of 
  25  the 'Field'. 
  26   
  27  Every 'Extension' class has a set of arguments composed of 'Field'.  An 
  28  instance of that 'Extension' class can be constructed by providing a 
  29  value for each 'Field' object.  The GUI can display the 'Extension' 
  30  object by rendering each of the 'Field' values as HTML.  The user can 
  31  change the value of a 'Field' in the GUI, and then write the 'Extension' 
  32  object to persistent storage. 
  33   
  34  Additional derived classes of 'Field' can be created for use in 
  35  domain-specific situations.  For example, the QMTest 'Test' class 
  36  defines a derived class which allows the user to select from among a set 
  37  of test names.""" 
  38   
  39  ######################################################################## 
  40  # imports 
  41  ######################################################################## 
  42   
  43  import attachment 
  44  import common 
  45  import formatter 
  46  import htmllib 
  47  import os 
  48  import re 
  49  import qm 
  50  import string 
  51  import StringIO 
  52  import structured_text 
  53  import sys 
  54  import time 
  55  import tokenize 
  56  import types 
  57  import urllib 
  58  import web 
  59  import xml.dom 
  60  import xmlutil 
  61   
  62  ######################################################################## 
  63  # classes 
  64  ######################################################################## 
  65   
66 -class Field(object):
67 """A 'Field' is a named, typed component of a data structure.""" 68 69 form_field_prefix = "_field_" 70
71 - def __init__(self, 72 name = "", 73 default_value = None, 74 title = "", 75 description = "", 76 hidden = "false", 77 read_only = "false", 78 computed = "false"):
79 """Create a new (generic) field. 80 81 'name' -- The name of the field. 82 83 'default_value' -- The default value for this field. 84 85 'title' -- The name given this field when it is displayed in 86 user interfaces. 87 88 'description' -- A string explaining the purpose of this field. 89 The 'description' must be provided as structured text. The 90 first line of the structured text must be a one-sentence 91 description of the field; that line is extracted by 92 'GetBriefDescription'. 93 94 'hidden' -- If true, this field is for internal puprpose only 95 and is not shown in user interfaces. 96 97 'read_only' -- If true, this field may not be modified by users. 98 99 'computed' -- If true, this field is computed automatically. 100 All computed fields are implicitly hidden and implicitly 101 read-only. 102 103 The boolean parameters (such as 'hidden') use the convention 104 that true is represented by the string '"true"'; any other value 105 is false. This convention is a historical artifact.""" 106 107 self.__name = name 108 # Use the name as the title, if no other was specified. 109 if not title: 110 self.__title = name 111 else: 112 self.__title = title 113 self.__description = description 114 self.__hidden = hidden == "true" 115 self.__read_only = read_only == "true" 116 self.__computed = computed == "true" 117 118 # All computed fields are also read-only and hidden. 119 if (self.IsComputed()): 120 self.__read_only = 1 121 self.__hidden = 1 122 123 self.__default_value = default_value
124 125
126 - def SetName(self, name):
127 """Set the name of the field.""" 128 129 # We assume that if title==name the title 130 # was not given and so defaulted to name. 131 # Keep it in sync with name in that case. 132 if (self.__name == self.__title): 133 self.__title = name 134 self.__name = name
135 136
137 - def GetName(self):
138 """Return the name of the field.""" 139 140 return self.__name
141 142
143 - def GetDefaultValue(self):
144 """Return the default value for this field.""" 145 146 return common.copy(self.__default_value)
147 148
149 - def GetTitle(self):
150 """Return the user-friendly title of the field.""" 151 152 return self.__title
153 154
155 - def GetDescription(self):
156 """Return a description of this field. 157 158 This description is used when displaying detailed help 159 information about the field.""" 160 161 return self.__description
162 163
164 - def GetBriefDescription(self):
165 """Return a brief description of this field. 166 167 This description is used when prompting for input, or when 168 displaying the current value of the field.""" 169 170 # Get the complete description. 171 description = self.GetDescription() 172 # Return the first paragraph. 173 return structured_text.get_first(description)
174 175
176 - def GetHelp(self):
177 """Generate help text about this field in structured text format.""" 178 179 raise NotImplementedError
180 181
182 - def GetHtmlHelp(self, edit=0):
183 """Generate help text about this field in HTML format. 184 185 'edit' -- If true, display information about editing controls 186 for this field.""" 187 188 description = structured_text.to_html(self.GetDescription()) 189 help = structured_text.to_html(self.GetHelp()) 190 191 return ''' 192 <h3>%s</h3> 193 <h4>About This Field</h4> 194 %s 195 <hr noshade size="2"> 196 <h4>About This Field\'s Values</h4> 197 %s 198 <hr noshade size="2"> 199 <p>Refer to this field as <tt>%s</tt> in Python expressions.</p> 200 ''' % (self.GetTitle(), description, help, self.GetName(), )
201 202
203 - def GetSubfields(self):
204 """Returns the sequence of subfields contained in this field. 205 206 returns -- The sequence of subfields contained in this field. 207 If there are no subfields, an empty sequence is returned.""" 208 209 return ()
210 211
212 - def IsComputed(self):
213 """Returns true if this field is computed automatically. 214 215 returns -- True if this field is computed automatically. A 216 computed field is never displayed to users and is not stored 217 should not be stored; the class containing the field is 218 responsible for recomputing it as necessary.""" 219 220 return self.__computed
221 222
223 - def IsHidden(self):
224 """Returns true if this 'Field' should be hidden from users. 225 226 returns -- True if this 'Field' should be hidden from users. 227 The value of a hidden field is not displayed in the GUI.""" 228 229 return self.__hidden
230 231
232 - def IsReadOnly(self):
233 """Returns true if this 'Field' cannot be modified by users. 234 235 returns -- True if this 'Field' cannot be modified by users. 236 The GUI does not allow users to modify a read-only field.""" 237 238 return self.__read_only
239 240 ### Output methods. 241
242 - def FormatValueAsText(self, value, columns=72):
243 """Return a plain text rendering of a 'value' for this field. 244 245 'columns' -- The maximum width of each line of text. 246 247 returns -- A plain-text string representing 'value'.""" 248 249 # Create a file to hold the result. 250 text_file = StringIO.StringIO() 251 # Format the field as HTML. 252 html_file = StringIO.StringIO(self.FormatValueAsHtml(None, 253 value, 254 "brief")) 255 256 # Turn the HTML into plain text. 257 parser = htmllib.HTMLParser(formatter.AbstractFormatter 258 (formatter.DumbWriter(text_file, 259 maxcol = columns))) 260 parser.feed(html_file) 261 parser.close() 262 text = text_file.getValue() 263 264 # Close the files. 265 html_file.close() 266 text_file.close() 267 268 return text
269 270
271 - def FormatValueAsHtml(self, server, value, style, name=None):
272 """Return an HTML rendering of a 'value' for this field. 273 274 'server' -- The 'WebServer' in which the HTML will be 275 displayed. 276 277 'value' -- The value for this field. May be 'None', which 278 renders a default value (useful for blank forms). 279 280 'style' -- The rendering style. Can be "full" or "brief" (both 281 read-only), or "new" or "edit" or "hidden". 282 283 'name' -- The name to use for the primary HTML form element 284 containing the value of this field, if 'style' specifies the 285 generation of form elements. If 'name' is 'None', the value 286 returned by 'GetHtmlFormFieldName()' should be used. 287 288 returns -- A string containing the HTML representation of 289 'value'.""" 290 291 raise NotImplementedError
292 293
294 - def MakeDomNodeForValue(self, value, document):
295 """Generate a DOM element node for a value of this field. 296 297 'value' -- The value to represent. 298 299 'document' -- The containing DOM document node.""" 300 301 raise NotImplementedError
302 303 ### Input methods. 304
305 - def Validate(self, value):
306 """Validate a field value. 307 308 For an acceptable type and value, return the representation of 309 'value' in the underlying field storage. 310 311 'value' -- A value to validate for this field. 312 313 returns -- If the 'value' is valid, returns 'value' or an 314 equivalent "canonical" version of 'value'. (For example, this 315 function may search a hash table and return an equivalent entry 316 from the hash table.) 317 318 This function must raise an exception if the value is not valid. 319 The string representation of the exception will be used as an 320 error message in some situations. 321 322 Implementations of this method must be idempotent.""" 323 324 raise NotImplementedError
325 326
327 - def ParseTextValue(self, value):
328 """Parse a value represented as a string. 329 330 'value' -- A string representing the value. 331 332 returns -- The corresponding field value. The value returned 333 should be processed by 'Validate' to ensure that it is valid 334 before it is returned.""" 335 336 raise NotImplemented
337 338
339 - def ParseFormValue(self, request, name, attachment_stores):
340 """Convert a value submitted from an HTML form. 341 342 'request' -- The 'WebRequest' containing a value corresponding 343 to this field. 344 345 'name' -- The name corresponding to this field in the 'request'. 346 347 'attachment_stores' -- A dictionary mapping 'AttachmentStore' ids 348 (in the sense of Python's 'id' built-in) to the 349 'AttachmentStore's themselves. 350 351 returns -- A pair '(value, redisplay)'. 'value' is the value 352 for this field, as indicated in 'request'. 'redisplay' is true 353 if and only if the form should be redisplayed, rather than 354 committed. If an error occurs, an exception is thrown.""" 355 356 # Retrieve the value provided in the form. 357 value = request[name] 358 # Treat the result as we would if it were provided on the 359 # command-line. 360 return (self.ParseTextValue(value), 0)
361 362
363 - def GetValueFromDomNode(self, node, attachment_store):
364 """Return a value for this field represented by DOM 'node'. 365 366 This method does not validate the value for this particular 367 instance; it only makes sure the node is well-formed, and 368 returns a value of the correct Python type. 369 370 'node' -- The DOM node that is being evaluated. 371 372 'attachment_store' -- For attachments, the store that should be 373 used. 374 375 If the 'node' is incorrectly formed, this method should raise an 376 exception.""" 377 378 raise NotImplementedError
379 380 # Other methods. 381
382 - def GetHtmlFormFieldName(self):
383 """Return the form field name corresponding this field. 384 385 returns -- A string giving the name that should be used for this 386 field when used in an HTML form.""" 387 388 return self.form_field_prefix + self.GetName()
389 390
391 - def __repr__(self):
392 393 # This output format is more useful when debugging than the 394 # default "<... instance at 0x...>" format provided by Python. 395 return "<%s %s>" % (self.__class__, self.GetName())
396 397 398 ######################################################################## 399
400 -class IntegerField(Field):
401 """An 'IntegerField' stores an 'int' or 'long' object.""" 402
403 - def __init__(self, name="", default_value=0, **properties):
404 """Construct a new 'IntegerField'. 405 406 'name' -- As for 'Field.__init__'. 407 408 'default_value' -- As for 'Field.__init__'. 409 410 'properties' -- Other keyword arguments for 'Field.__init__'.""" 411 412 # Perform base class initialization. 413 super(IntegerField, self).__init__(name, default_value, **properties)
414 415
416 - def GetHelp(self):
417 418 return """This field stores an integer. 419 420 The default value of this field is %d."""
421 422 ### Output methods. 423
424 - def FormatValueAsText(self, value, columns=72):
425 426 return str(value)
427 428
429 - def FormatValueAsHtml(self, server, value, style, name=None):
430 # Use default value if requested. 431 if value is None: 432 value = self.GetDefaultValue() 433 # Use the default field form field name if requested. 434 if name is None: 435 name = self.GetHtmlFormFieldName() 436 437 if style == "new" or style == "edit": 438 return '<input type="text" size="8" name="%s" value="%d" />' \ 439 % (name, value) 440 elif style == "full" or style == "brief": 441 return '<tt>%d</tt>' % value 442 elif style == "hidden": 443 return '<input type="hidden" name="%s" value="%d" />' \ 444 % (name, value) 445 else: 446 assert None
447 448
449 - def MakeDomNodeForValue(self, value, document):
450 return xmlutil.create_dom_text_element(document, "integer", 451 str(value))
452 453 454 ### Input methods. 455
456 - def Validate(self, value):
457 458 if not isinstance(value, (int, long)): 459 raise ValueError, value 460 461 return value
462 463
464 - def ParseTextValue(self, value):
465 466 try: 467 return self.Validate(int(value)) 468 except: 469 raise qm.common.QMException, \ 470 qm.error("invalid integer field value")
471 472
473 - def GetValueFromDomNode(self, node, attachment_store):
474 475 # Make sure 'node' is an '<integer>' element. 476 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 477 or node.tagName != "integer": 478 raise qm.QMException, \ 479 qm.error("dom wrong tag for field", 480 name=self.GetName(), 481 right_tag="integer", 482 wrong_tag=node.tagName) 483 # Retrieve the contained text. 484 value = xmlutil.get_dom_text(node) 485 # Convert it to an integer. 486 return self.ParseTextValue(value)
487 488 489 ######################################################################## 490
491 -class TextField(Field):
492 """A field that contains text.""" 493
494 - def __init__(self, 495 name = "", 496 default_value = "", 497 multiline = "false", 498 structured = "false", 499 verbatim = "false", 500 not_empty_text = "false", 501 **properties):
502 """Construct a new 'TextField'. 503 504 'multiline' -- If false, a value for this field is a single line 505 of text. If true, multi-line text is allowed. 506 507 'structured' -- If true, the field contains structured text. 508 509 'verbatim' -- If true, the contents of the field are treated as 510 preformatted text. 511 512 'not_empty_text' -- The value of this field is considered 513 invalid if it empty or composed only of whitespace. 514 515 'properties' -- A dictionary of other keyword arguments which 516 are provided to the base class constructor.""" 517 518 # Initialize the base class. 519 super(TextField, self).__init__(name, default_value, **properties) 520 521 self.__multiline = multiline == "true" 522 self.__structured = structured == "true" 523 self.__verbatim = verbatim == "true" 524 self.__not_empty_text = not_empty_text == "true"
525 526
527 - def GetHelp(self):
528 529 help = """ 530 A text field. """ 531 if self.__structured: 532 help = help + ''' 533 The text is interpreted as structured text, and formatted 534 appropriately for the output device. See "Structured Text 535 Formatting 536 Rules":http://www.python.org/sigs/doc-sig/stext.html for 537 more information. ''' 538 elif self.__verbatim: 539 help = help + """ 540 The text is stored verbatim; whitespace and indentation are 541 preserved. """ 542 if self.__not_empty_text: 543 help = help + """ 544 This field may not be empty. """ 545 help = help + """ 546 The default value of this field is "%s". 547 """ % self.GetDefaultValue() 548 return help
549 550 ### Output methods. 551
552 - def FormatValueAsText(self, value, columns=72):
553 554 if self.__structured: 555 return structured_text.to_text(value, width=columns) 556 elif self.__verbatim: 557 return value 558 else: 559 return common.wrap_lines(value, columns)
560 561
562 - def FormatValueAsHtml(self, server, value, style, name=None):
563 564 # Use default value if requested. 565 if value is None: 566 value = "" 567 else: 568 value = str(value) 569 # Use the default field form field name if requested. 570 if name is None: 571 name = self.GetHtmlFormFieldName() 572 573 if style == "new" or style == "edit": 574 if self.__multiline: 575 result = '<textarea cols="64" rows="8" name="%s">' \ 576 '%s</textarea>' \ 577 % (name, web.escape(value)) 578 else: 579 result = \ 580 '<input type="text" size="40" name="%s" value="%s" />' \ 581 % (name, web.escape(value)) 582 # If this is a structured text field, add a note to that 583 # effect, so users aren't surprised. 584 if self.__structured: 585 result = result \ 586 + '<br><font size="-1">This is a ' \ 587 + qm.web.make_help_link_html( 588 qm.structured_text.html_help_text, 589 "structured text") \ 590 + 'field.</font>' 591 return result 592 593 elif style == "hidden": 594 return '<input type="hidden" name="%s" value="%s" />' \ 595 % (name, web.escape(value)) 596 597 elif style == "brief": 598 if self.__structured: 599 # Use only the first line of text. 600 value = string.split(value, "\n", 1) 601 value = web.format_structured_text(value[0]) 602 else: 603 # Replace all whitespace with ordinary space. 604 value = re.sub(r"\s", " ", value) 605 606 # Truncate to 80 characters, if it's longer. 607 if len(value) > 80: 608 value = value[:80] + "..." 609 610 if self.__verbatim: 611 # Put verbatim text in a <tt> element. 612 return '<tt>%s</tt>' % web.escape(value) 613 elif self.__structured: 614 # It's already formatted as HTML; don't escape it. 615 return value 616 else: 617 # Other text set normally. 618 return web.escape(value) 619 620 elif style == "full": 621 if self.__verbatim: 622 # Wrap lines before escaping special characters for 623 # HTML. Use a special tag to indicate line breaks. If 624 # we were to escape first, line lengths would be 625 # computed using escape codes rather than visual 626 # characters. 627 break_delimiter = "#@LINE$BREAK@#" 628 value = common.wrap_lines(value, columns=80, 629 break_delimiter=break_delimiter) 630 # Now escape special characters. 631 value = web.escape(value) 632 # Replace the line break tag with visual indication of 633 # the break. 634 value = string.replace(value, 635 break_delimiter, r"<blink>\</blink>") 636 # Place verbatim text in a <pre> element. 637 return '<pre>%s</pre>' % value 638 elif self.__structured: 639 return web.format_structured_text(value) 640 else: 641 if value == "": 642 # Browsers don't deal nicely with empty table cells, 643 # so put an extra space here. 644 return "&nbsp;" 645 else: 646 return web.escape(value) 647 648 else: 649 raise ValueError, style
650 651
652 - def MakeDomNodeForValue(self, value, document):
653 654 return xmlutil.create_dom_text_element(document, "text", value)
655 656 ### Input methods. 657
658 - def Validate(self, value):
659 660 if not isinstance(value, types.StringTypes): 661 raise ValueError, value 662 663 # Clean up unless it's a verbatim string. 664 if not self.__verbatim: 665 # Remove leading whitespace. 666 value = string.lstrip(value) 667 # If this field has the not_empty_text property set, make sure the 668 # value complies. 669 if self.__not_empty_text and value == "": 670 raise ValueError, \ 671 qm.error("empty text field value", 672 field_title=self.GetTitle()) 673 # If this is not a multi-line text field, remove line breaks 674 # (and surrounding whitespace). 675 if not self.__multiline: 676 value = re.sub(" *\n+ *", " ", value) 677 return value
678 679
680 - def ParseFormValue(self, request, name, attachment_stores):
681 682 # HTTP specifies text encodings are CR/LF delimited; convert to 683 # the One True Text Format (TM). 684 return (self.ParseTextValue(qm.convert_from_dos_text(request[name])), 685 0)
686 687
688 - def ParseTextValue(self, value):
689 690 return self.Validate(value)
691 692
693 - def GetValueFromDomNode(self, node, attachment_store):
694 695 # Make sure 'node' is a '<text>' element. 696 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 697 or node.tagName != "text": 698 raise qm.QMException, \ 699 qm.error("dom wrong tag for field", 700 name=self.GetName(), 701 right_tag="text", 702 wrong_tag=node.tagName) 703 return self.Validate(xmlutil.get_dom_text(node))
704 705 706 ######################################################################## 707
708 -class TupleField(Field):
709 """A 'TupleField' contains zero or more other 'Field' objects. 710 711 The contained 'Field' objects may have different types. The value 712 of a 'TupleField' is a Python list; the values in the list 713 correspond to the values of the contained 'Field' objects. For 714 example, '["abc", 3]' would be a valid value for a 'TupleField' 715 containing a 'TextField' and an 'IntegerField'.""" 716
717 - def __init__(self, name = "", fields = None, **properties):
718 """Construct a new 'TupleField'. 719 720 'name' -- The name of the field. 721 722 'fields' -- A sequence of 'Field' instances. 723 724 The new 'TupleField' stores a list whose elements correspond to 725 the 'fields'.""" 726 727 self.__fields = fields == None and [] or fields 728 default_value = map(lambda f: f.GetDefaultValue(), self.__fields) 729 Field.__init__(self, name, default_value, **properties)
730 731
732 - def GetHelp(self):
733 734 help = "" 735 need_space = 0 736 for f in self.__fields: 737 if need_space: 738 help += "\n" 739 else: 740 need_space = 1 741 help += "** " + f.GetTitle() + " **\n\n" 742 help += f.GetHelp() 743 744 return help
745 746
747 - def GetSubfields(self):
748 749 return self.__fields
750 751 752 ### Output methods. 753
754 - def FormatValueAsHtml(self, server, value, style, name = None):
755 756 # Use the default name if none is specified. 757 if name is None: 758 name = self.GetHtmlFormFieldName() 759 760 # Format the field as a multi-column table. 761 html = '<table border="0" cellpadding="0">\n <tr>\n' 762 for f, v in map(None, self.__fields, value): 763 element_name = name + "_" + f.GetName() 764 html += " <td><b>" + f.GetTitle() + "</b>:</td>\n" 765 html += (" <td>\n" 766 + f.FormatValueAsHtml(server, v, style, element_name) 767 + " </td>\n") 768 html += " </tr>\n</table>\n" 769 770 return html
771 772
773 - def MakeDomNodeForValue(self, value, document):
774 775 element = document.createElement("tuple") 776 for f, v in map(None, self.__fields, value): 777 element.appendChild(f.MakeDomNodeForValue(v, document)) 778 779 return element
780 781 ### Input methods. 782
783 - def Validate(self, value):
784 785 assert len(value) == len(self.__fields) 786 return map(lambda f, v: f.Validate(v), 787 self.__fields, value)
788 789
790 - def ParseFormValue(self, request, name, attachment_stores):
791 792 value = [] 793 redisplay = 0 794 for f in self.__fields: 795 v, r = f.ParseFormValue(request, name + "_" + f.GetName(), 796 attachment_stores) 797 value.append(v) 798 if r: 799 redisplay = 1 800 801 # Now that we've computed the value of the entire tuple, make 802 # sure it is valid. 803 value = self.Validate(value) 804 805 return (value, redisplay)
806 807
808 - def GetValueFromDomNode(self, node, attachment_store):
809 810 values = [] 811 for f, element in map(None, self.__fields, node.childNodes): 812 values.append(f.GetValueFromDomNode(element, attachment_store)) 813 814 return self.Validate(values)
815 816 817
818 -class DictionaryField(Field):
819 """A 'DictionaryField' maps keys to values.""" 820
821 - def __init__(self, key_field, value_field, **properties):
822 """Construct a new 'DictionaryField'. 823 824 'key_field' -- The key field. 825 826 'value_field' -- The value field. 827 """ 828 829 self.__key_field = key_field 830 self.__value_field = value_field 831 super(DictionaryField, self).__init__(**properties)
832 833
834 - def GetHelp(self):
835 836 help = """ 837 A dictionary field. A dictionary maps keys to values. The key type: 838 %s 839 The value type: 840 %s"""%(self.__key_field.GetHelp(), self.__value_field.GetHelp()) 841 return help
842 843
844 - def GetKeyField(self): return self.__key_field
845 - def GetValueField(self): return self.__value_field
846 847 ### Output methods. 848
849 - def FormatValueAsHtml(self, server, content, style, name = None):
850 851 if content is None: 852 content = {} 853 # Use the default name if none is specified. 854 if name is None: 855 name = self.GetHtmlFormFieldName() 856 857 if style == 'brief' or style == 'full': 858 if len(content) == 0: 859 # An empty set. 860 return 'None' 861 body = ['<th>%s</th><td>%s</td>\n' 862 %(self.__key_field.FormatValueAsHtml(server, key, style), 863 self.__value_field.FormatValueAsHtml(server, value, style)) 864 for (key, value) in content.iteritems()] 865 return '<table><tr>%s</tr>\n</table>\n'%'</tr>\n<tr>'.join(body) 866 867 elif style in ['new', 'edit', 'hidden']: 868 html = '' 869 if content: 870 # Create a table to represent the dictionary -- but only if it is 871 # non-empty. A table with no body is invalid HTML. 872 html += ('<table border="0" cellpadding="0" cellspacing="0">' 873 '\n <tbody>\n') 874 element_number = 0 875 for key, value in content.iteritems(): 876 html += ' <tr>\n <td>' 877 element_name = name + '_%d' % element_number 878 checkbox_name = element_name + "_remove" 879 if style == 'edit': 880 html += ('<input type="checkbox" name="%s" /></td>\n' 881 ' <td>\n' 882 % checkbox_name) 883 element_name = name + '_key_%d' % element_number 884 html += (' <th>%s</th>\n' 885 %self.__key_field.FormatValueAsHtml(server, key, 886 style, 887 element_name)) 888 element_name = name + '_value_%d' % element_number 889 html += (' <td>%s</td>\n' 890 %self.__value_field.FormatValueAsHtml(server, value, 891 style, 892 element_name)) 893 html += ' </tr>\n' 894 element_number += 1 895 html += ' </tbody>\n</table>\n' 896 # The action field is used to keep track of whether the 897 # "Add" or "Remove" button has been pushed. It would be 898 # much nice if we could use JavaScript to update the 899 # table, but Netscape 4, and even Mozilla 1.0, do not 900 # permit that. Therefore, we have to go back to the server. 901 html += '<input type="hidden" name="%s" value="" />' % name 902 html += ('<input type="hidden" name="%s_count" value="%d" />' 903 % (name, len(content))) 904 if style != 'hidden': 905 html += ('<table border="0" cellpadding="0" cellspacing="0">\n' 906 ' <tbody>\n' 907 ' <tr>\n' 908 ' <td><input type="button" name="%s_add" ' 909 'value="Add Another" ' 910 '''onclick="%s.value = 'add'; submit();" />''' 911 '</td>\n' 912 ' <td><input type="button" name="%s_remove"' 913 'value="Remove Selected" ' 914 '''onclick="%s.value = 'remove'; submit();" />''' 915 '</td>\n' 916 ' </tr>' 917 ' </tbody>' 918 '</table>' 919 % (name, name, name, name)) 920 return html
921 922
923 - def MakeDomNodeForValue(self, value, document):
924 925 element = document.createElement('dictionary') 926 for k, v in value.iteritems(): 927 item = element.appendChild(document.createElement('item')) 928 item.appendChild(self.__key_field.MakeDomNodeForValue(k, document)) 929 item.appendChild(self.__value_field.MakeDomNodeForValue(v, document)) 930 return element
931 932 933 ### Input methods. 934
935 - def Validate(self, value):
936 937 valid = {} 938 for k, v in value.items(): 939 valid[self.__key_field.Validate(k)] = self.__value_field.Validate(v) 940 941 return valid
942 943
944 - def ParseTextValue(self, value):
945 946 raise NotImplementedError
947 948
949 - def ParseFormValue(self, request, name, attachment_stores):
950 951 content = {} 952 redisplay = 0 953 954 action = request[name] 955 956 for i in xrange(int(request[name + '_count'])): 957 if not (action == 'remove' 958 and request.get(name + '_%d_remove'%i) == 'on'): 959 key, rk = self.__key_field.ParseFormValue(request, 960 name + '_key_%d'%i, 961 attachment_stores) 962 value, rv = self.__value_field.ParseFormValue(request, 963 name + '_value_%d'%i, 964 attachment_stores) 965 content[key] = value 966 if rk or rv: 967 redisplay = 1 968 969 # Remove entries from the request that might cause confusion 970 # when the page is redisplayed. 971 names = [] 972 for n, v in request.items(): 973 if n[:len(name)] == name: 974 names.append(n) 975 for n in names: 976 del request[n] 977 978 content = self.Validate(content) 979 980 if action == 'add': 981 redisplay = 1 982 content[self.__key_field.GetDefaultValue()] =\ 983 self.__value_field.GetDefaultValue() 984 elif action == 'remove': 985 redisplay = 1 986 987 return (content, redisplay)
988 989
990 - def GetValueFromDomNode(self, node, attachment_store):
991 992 values = {} 993 for item in node.childNodes: 994 if item.nodeType == xml.dom.Node.ELEMENT_NODE: 995 values[self.__key_field.GetValueFromDomNode 996 (item.childNodes[0], attachment_store)] =\ 997 self.__value_field.GetValueFromDomNode(item.childNodes[1], 998 attachment_store) 999 return self.Validate(values)
1000 1001 1002
1003 -class SetField(Field):
1004 """A field containing zero or more instances of some other field. 1005 1006 All contents must be of the same field type. A set field may not 1007 contain sets. 1008 1009 The default field value is set to an empty set.""" 1010
1011 - def __init__(self, contained, not_empty_set = "false", default_value = None, 1012 **properties):
1013 """Create a set field. 1014 1015 The name of the contained field is taken as the name of this 1016 field. 1017 1018 'contained' -- An 'Field' instance describing the 1019 elements of the set. 1020 1021 'not_empty_set' -- If true, this field may not be empty, 1022 i.e. the value of this field must contain at least one element. 1023 1024 raises -- 'ValueError' if 'contained' is a set field. 1025 1026 raises -- 'TypeError' if 'contained' is not a 'Field'.""" 1027 1028 if not properties.has_key('description'): 1029 properties['description'] = contained.GetDescription() 1030 1031 super(SetField, self).__init__( 1032 contained.GetName(), 1033 default_value or [], 1034 title = contained.GetTitle(), 1035 **properties) 1036 1037 # A set field may not contain a set field. 1038 if isinstance(contained, SetField): 1039 raise ValueError, \ 1040 "A set field may not contain a set field." 1041 if not isinstance(contained, Field): 1042 raise TypeError, "A set must contain another field." 1043 # Remeber the contained field type. 1044 self.__contained = contained 1045 self.__not_empty_set = not_empty_set == "true"
1046 1047
1048 - def GetHelp(self):
1049 return """ 1050 A set field. A set contains zero or more elements, all of the 1051 same type. The elements of the set are described below: 1052 1053 """ + self.__contained.GetHelp()
1054 1055
1056 - def GetSubfields(self):
1057 1058 return (self.__contained,)
1059 1060
1061 - def GetHtmlHelp(self, edit=0):
1062 help = Field.GetHtmlHelp(self) 1063 if edit: 1064 # In addition to the standard generated help, include 1065 # additional instructions about using the HTML controls. 1066 help = help + """ 1067 <hr noshade size="2"> 1068 <h4>Modifying This Field</h4> 1069 1070 <p>Add a new element to the set by clicking the 1071 <i>Add</i> button. The new element will have a default 1072 value until you change it. To remove elements from the 1073 set, select them by checking the boxes on the left side of 1074 the form. Then, click the <i>Remove</i> button.</p> 1075 """ 1076 return help
1077 1078 ### Output methods. 1079
1080 - def FormatValueAsText(self, value, columns=72):
1081 # If the set is empty, indicate this specially. 1082 if len(value) == 0: 1083 return "None" 1084 # Format each element of the set, and join them into a 1085 # comma-separated list. 1086 contained_field = self.__contained 1087 formatted_items = [] 1088 for item in value: 1089 formatted_item = contained_field.FormatValueAsText(item, columns) 1090 formatted_items.append(repr(formatted_item)) 1091 result = "[ " + string.join(formatted_items, ", ") + " ]" 1092 return qm.common.wrap_lines(result, columns)
1093 1094
1095 - def FormatValueAsHtml(self, server, value, style, name=None):
1096 # Use default value if requested. 1097 if value is None: 1098 value = [] 1099 # Use the default field form field name if requested. 1100 if name is None: 1101 name = self.GetHtmlFormFieldName() 1102 1103 contained_field = self.__contained 1104 if style == "brief" or style == "full": 1105 if len(value) == 0: 1106 # An empty set. 1107 return "None" 1108 formatted \ 1109 = map(lambda v: contained_field.FormatValueAsHtml(server, 1110 v, style), 1111 value) 1112 if style == "brief": 1113 # In the brief style, list elements separated by commas. 1114 separator = ", " 1115 else: 1116 # In the full style, list elements one per line. 1117 separator = "<br>\n" 1118 return string.join(formatted, separator) 1119 1120 elif style in ["new", "edit", "hidden"]: 1121 html = "" 1122 if value: 1123 # Create a table to represent the set -- but only if the set is 1124 # non-empty. A table with no body is invalid HTML. 1125 html += ('<table border="0" cellpadding="0" cellspacing="0">' 1126 "\n <tbody>\n") 1127 element_number = 0 1128 for element in value: 1129 html += " <tr>\n <td>" 1130 element_name = name + "_%d" % element_number 1131 checkbox_name = element_name + "_remove" 1132 if style == "edit": 1133 html += \ 1134 ('<input type="checkbox" name="%s" /></td>\n' 1135 ' <td>\n' 1136 % checkbox_name) 1137 html += contained_field.FormatValueAsHtml(server, 1138 element, 1139 style, 1140 element_name) 1141 html += " </td>\n </tr>\n" 1142 element_number += 1 1143 html += " </tbody>\n</table>\n" 1144 # The action field is used to keep track of whether the 1145 # "Add" or "Remove" button has been pushed. It would be 1146 # much nice if we could use JavaScript to update the 1147 # table, but Netscape 4, and even Mozilla 1.0, do not 1148 # permit that. Therefore, we have to go back to the server. 1149 html += '<input type="hidden" name="%s" value="" />' % name 1150 html += ('<input type="hidden" name="%s_count" value="%d" />' 1151 % (name, len(value))) 1152 if style != "hidden": 1153 html += ('<table border="0" cellpadding="0" cellspacing="0">\n' 1154 ' <tbody>\n' 1155 ' <tr>\n' 1156 ' <td><input type="button" name="%s_add" ' 1157 'value="Add Another" ' 1158 '''onclick="%s.value = 'add'; submit();" />''' 1159 '</td>\n' 1160 ' <td><input type="button" name="%s_remove"' 1161 'value="Remove Selected" ' 1162 '''onclick="%s.value = 'remove'; submit();" />''' 1163 '</td>\n' 1164 ' </tr>' 1165 ' </tbody>' 1166 '</table>' 1167 % (name, name, name, name)) 1168 return html
1169 1170
1171 - def MakeDomNodeForValue(self, value, document):
1172 1173 # Create a set element. 1174 element = document.createElement("set") 1175 # Add a child node for each item in the set. 1176 contained_field = self.__contained 1177 for item in value: 1178 # The contained field knows how to make a DOM node for each 1179 # item in the set. 1180 item_node = contained_field.MakeDomNodeForValue(item, document) 1181 element.appendChild(item_node) 1182 return element
1183 1184 ### Input methods. 1185
1186 - def Validate(self, value):
1187 1188 # If this field has the not_empty_set property set, make sure 1189 # the value complies. 1190 if self.__not_empty_set and len(value) == 0: 1191 raise ValueError, \ 1192 qm.error("empty set field value", 1193 field_title=self.GetTitle()) 1194 # Assume 'value' is a sequence. Copy it, simultaneously 1195 # validating each element in the contained field. 1196 return map(lambda v: self.__contained.Validate(v), 1197 value)
1198 1199
1200 - def ParseTextValue(self, value):
1201
1202 - def invalid(tok):
1203 """Raise an exception indicating a problem with 'value'. 1204 1205 'tok' -- A token indicating the position of the problem. 1206 1207 This function does not return; instead, it raises an 1208 appropriate exception.""" 1209 1210 raise qm.QMException, \ 1211 qm.error("invalid set value", start = value[tok[2][1]:])
1212 1213 # Use the Python parser to handle the elements of the set. 1214 s = StringIO.StringIO(value) 1215 g = tokenize.generate_tokens(s.readline) 1216 1217 # Read the opening square bracket. 1218 tok = g.next() 1219 if tok[0] != tokenize.OP or tok[1] != "[": 1220 invalid(tok) 1221 1222 # There are no elements yet. 1223 elements = [] 1224 1225 # Keep going until we find the closing bracket. 1226 while 1: 1227 # If we've reached the closing bracket, the set is 1228 # complete. 1229 tok = g.next() 1230 if tok[0] == tokenize.OP and tok[1] == "]": 1231 break 1232 # If this is not the first element of the set, there should 1233 # be a comma before the next element. 1234 if elements: 1235 if tok[0] != tokenize.OP or tok[1] != ",": 1236 invalid(tok) 1237 tok = g.next() 1238 # The next token should be a string constant. 1239 if tok[0] != tokenize.STRING: 1240 invalid(tok) 1241 # Parse the string constant. 1242 v = eval(tok[1]) 1243 elements.append(self.__contained.ParseTextValue(v)) 1244 1245 # There should not be any tokens left over. 1246 tok = g.next() 1247 if not tokenize.ISEOF(tok[0]): 1248 invalid(tok) 1249 1250 return self.Validate(elements)
1251 1252
1253 - def ParseFormValue(self, request, name, attachment_stores):
1254 1255 values = [] 1256 redisplay = 0 1257 1258 # See if the user wants to add or remove elements from the set. 1259 action = request[name] 1260 # Loop over the entries for each of the elements, adding them to 1261 # the set. 1262 contained_field = self.__contained 1263 element = 0 1264 for element in xrange(int(request[name + "_count"])): 1265 element_name = name + "_%d" % element 1266 if not (action == "remove" 1267 and request.get(element_name + "_remove") == "on"): 1268 v, r = contained_field.ParseFormValue(request, 1269 element_name, 1270 attachment_stores) 1271 values.append(v) 1272 if r: 1273 redisplay = 1 1274 element += 1 1275 1276 # Remove entries from the request that might cause confusion 1277 # when the page is redisplayed. 1278 names = [] 1279 for n, v in request.items(): 1280 if n[:len(name)] == name: 1281 names.append(n) 1282 for n in names: 1283 del request[n] 1284 1285 # Validate the values. 1286 values = self.Validate(values) 1287 1288 # If the user requested another element, add to the set. 1289 if action == "add": 1290 redisplay = 1 1291 # There's no need to validate this new value and it may in 1292 # fact be dangerous to do so. For example, the default 1293 # value for a ChoiceField might be the "nothing selected" 1294 # value, which is not a valid selection. If the user does 1295 # not actually select something, the problem will be 1296 # reported when the form is submitted. 1297 values.append(contained_field.GetDefaultValue()) 1298 elif action == "remove": 1299 redisplay = 1 1300 1301 return (values, redisplay)
1302 1303
1304 - def GetValueFromDomNode(self, node, attachment_store):
1305 # Make sure 'node' is a '<set>' element. 1306 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1307 or node.tagName != "set": 1308 raise qm.QMException, \ 1309 qm.error("dom wrong tag for field", 1310 name=self.GetName(), 1311 right_tag="set", 1312 wrong_tag=node.tagName) 1313 # Use the contained field to extract values for the children of 1314 # this node, which are the set elements. 1315 contained_field = self.__contained 1316 fn = lambda n, f=contained_field, s=attachment_store: \ 1317 f.GetValueFromDomNode(n, s) 1318 values = map(fn, 1319 filter(lambda n: n.nodeType == xml.dom.Node.ELEMENT_NODE, 1320 node.childNodes)) 1321 return self.Validate(values)
1322 1323 1324 1325 ######################################################################## 1326
1327 -class UploadAttachmentPage(web.DtmlPage):
1328 """DTML context for generating upload-attachment.dtml.""" 1329
1330 - def __init__(self, 1331 attachment_store, 1332 field_name, 1333 encoding_name, 1334 summary_field_name, 1335 in_set=0):
1336 """Create a new page object. 1337 1338 'attachment_store' -- The AttachmentStore in which the new 1339 attachment will be placed. 1340 1341 'field_name' -- The user-visible name of the field for which an 1342 attachment is being uploaded. 1343 1344 'encoding_name' -- The name of the HTML input that should 1345 contain the encoded attachment. 1346 1347 'summary_field_name' -- The name of the HTML input that should 1348 contain the user-visible summary of the attachment. 1349 1350 'in_set' -- If true, the attachment is being added to an 1351 attachment set field.""" 1352 1353 web.DtmlPage.__init__(self, "attachment.dtml") 1354 # Use a brand-new location for the attachment data. 1355 self.location = attachment.make_temporary_location() 1356 # Set up properties. 1357 self.attachment_store_id = id(attachment_store) 1358 self.field_name = field_name 1359 self.encoding_name = encoding_name 1360 self.summary_field_name = summary_field_name 1361 self.in_set = in_set
1362 1363
1364 - def MakeSubmitUrl(self):
1365 """Return the URL for submitting this form.""" 1366 1367 return self.request.copy(AttachmentField.upload_url).AsUrl()
1368 1369 1370
1371 -class AttachmentField(Field):
1372 """A field containing a file attachment. 1373 1374 Note that the 'FormatValueAsHtml' method uses a popup upload form 1375 for uploading new attachment. The web server must be configured to 1376 handle the attachment submission requests. See 1377 'attachment.register_attachment_upload_script'.""" 1378 1379 upload_url = "/attachment-upload" 1380 """The URL used to upload data for an attachment. 1381 1382 The upload request will include these query arguments: 1383 1384 'location' -- The location at which to store the attachment data. 1385 1386 'file_data' -- The attachment data. 1387 1388 """ 1389 1390 download_url = "/attachment-download" 1391 """The URL used to download an attachment. 1392 1393 The download request will include this query argument: 1394 1395 'location' -- The location in the attachment store from which to 1396 retrieve the attachment data. 1397 1398 """ 1399 1400
1401 - def __init__(self, name = "", **properties):
1402 """Create an attachment field. 1403 1404 Sets the default value of the field to 'None'.""" 1405 1406 # Perform base class initialization. 1407 apply(Field.__init__, (self, name, None), properties)
1408 1409
1410 - def GetHelp(self):
1411 return """ 1412 An attachment field. An attachment consists of an uploaded 1413 file, which may be of any file type, plus a short description. 1414 The name of the file, as well as the file's MIME type, are also 1415 stored. The description is a single line of plain text. 1416 1417 An attachment need not be provided. The field may be left 1418 empty."""
1419 1420
1421 - def GetHtmlHelp(self, edit=0):
1422 help = Field.GetHtmlHelp(self) 1423 if edit: 1424 # In addition to the standard generated help, include 1425 # additional instructions about using the HTML controls. 1426 help = help + """ 1427 <hr noshade size="2"> 1428 <h4>Modifying This Field</h4> 1429 1430 <p>The text control describes the current value of this 1431 field, displaying the attachment's description, file name, 1432 and MIME type. If the field is empty, the text control 1433 displays "None". The text control cannot be edited.</p> 1434 1435 <p>To upload a new attachment (replacing the previous one, 1436 if any), click on the <i>Change...</i> button. To clear the 1437 current attachment and make the field empty, click on the 1438 <i>Clear</i> button.</p> 1439 """ 1440 return help
1441 1442 ### Output methods. 1443
1444 - def FormatValueAsText(self, value, columns=72):
1445 1446 return self._FormatSummary(value)
1447 1448
1449 - def FormatValueAsHtml(self, server, value, style, name=None):
1450 1451 field_name = self.GetName() 1452 1453 if value is None: 1454 # The attachment field value may be 'None', indicating no 1455 # attachment. 1456 pass 1457 elif isinstance(value, attachment.Attachment): 1458 location = value.GetLocation() 1459 mime_type = value.GetMimeType() 1460 description = value.GetDescription() 1461 file_name = value.GetFileName() 1462 else: 1463 raise ValueError, "'value' must be 'None' or an 'Attachment'" 1464 1465 # Use the default field form field name if requested. 1466 if name is None: 1467 name = self.GetHtmlFormFieldName() 1468 1469 if style == "full" or style == "brief": 1470 if value is None: 1471 return "None" 1472 # Link the attachment description to the data itself. 1473 download_url = web.WebRequest(self.download_url, 1474 location=location, 1475 mime_type=mime_type).AsUrl() 1476 # Here's a nice hack. If the user saves the attachment to a 1477 # file, browsers (some at least) guess the default file name 1478 # from the URL by taking everything following the final 1479 # slash character. So, we add this bogus-looking argument 1480 # to fool the browser into using our file name. 1481 download_url = download_url + \ 1482 "&=/" + urllib.quote_plus(file_name) 1483 1484 result = '<a href="%s">%s</a>' \ 1485 % (download_url, description) 1486 # For the full style, display the MIME type. 1487 if style == "full": 1488 result = result + ' (%s)' % (mime_type) 1489 return result 1490 1491 elif style == "new" or style == "edit": 1492 1493 # Some trickiness here. 1494 # 1495 # For attachment fields, the user specifies the file to 1496 # upload via a popup form, which is shown in a new browser 1497 # window. When that form is submitted, the attachment data 1498 # is immediately uploaded to the server. 1499 # 1500 # The information that's stored for an attachment is made of 1501 # four parts: a description, a MIME type, the file name, and 1502 # the location of the data itself. The user enters these 1503 # values in the popup form, which sets a hidden field on 1504 # this form to an encoding of that information. 1505 # 1506 # Also, when the popup form is submitted, the attachment 1507 # data is uploaded. By the time this form is submitted, the 1508 # attachment data should be uploaded already. The uploaded 1509 # attachment data is stored in the temporary attachment 1510 # area; it's copied into the IDB when the issue revision is 1511 # submitted. 1512 1513 summary_field_name = "_attachment" + name 1514 1515 # Fill in the description if there's already an attachment. 1516 summary_value = 'value="%s"' % self._FormatSummary(value) 1517 if value is None: 1518 field_value = "" 1519 else: 1520 # We'll encode all the relevant information. 1521 parts = (description, mime_type, location, file_name, 1522 str(id(value.GetStore()))) 1523 # Each part is URL-encoded. 1524 parts = map(urllib.quote, parts) 1525 # The parts are joined into a semicolon-delimited list. 1526 field_value = string.join(parts, ";") 1527 field_value = 'value="%s"' % field_value 1528 1529 # Generate the popup upload page. 1530 upload_page = \ 1531 UploadAttachmentPage(server.GetTemporaryAttachmentStore(), 1532 self.GetTitle(), 1533 name, 1534 summary_field_name)() 1535 1536 # Generate controls for this form. 1537 1538 # A text control for the user-visible summary of the 1539 # attachment. The "readonly" property isn't supported in 1540 # Netscape, so prevent the user from typing into the form by 1541 # forcing focus away from the control. 1542 text_control = ''' 1543 <input type="text" 1544 readonly 1545 size="40" 1546 name="%s" 1547 onfocus="this.blur();" 1548 %s>''' % (summary_field_name, summary_value) 1549 # A button to pop up the upload form. It causes the upload 1550 # page to appear in a popup window. 1551 upload_button \ 1552 = server.MakeButtonForCachedPopup("Upload", 1553 upload_page, 1554 window_width=640, 1555 window_height=320) 1556 # A button to clear the attachment. 1557 clear_button = ''' 1558 <input type="button" 1559 size="20" 1560 value=" Clear " 1561 name="_clear_%s" 1562 onclick="document.form.%s.value = 'None'; 1563 document.form.%s.value = '';" /> 1564 ''' % (field_name, summary_field_name, name) 1565 # A hidden control for the encoded attachment value. The 1566 # popup upload form fills in this control. 1567 hidden_control = ''' 1568 <input type="hidden" 1569 name="%s" 1570 %s>''' % (name, field_value) 1571 # Now assemble the controls with some layout bits. 1572 result = ''' 1573 %s%s<br> 1574 %s%s 1575 ''' % (text_control, hidden_control, upload_button, clear_button) 1576 1577 return result 1578 1579 else: 1580 raise ValueError, style
1581 1582
1583 - def MakeDomNodeForValue(self, value, document):
1584 return attachment.make_dom_node(value, document)
1585 1586
1587 - def _FormatSummary(self, attachment):
1588 """Generate a user-friendly summary for 'attachment'. 1589 1590 This value is used when generating the form. It can't be 1591 editied.""" 1592 1593 if attachment is None: 1594 return "None" 1595 else: 1596 return "%s (%s; %s)" \ 1597 % (attachment.GetDescription(), 1598 attachment.GetFileName(), 1599 attachment.GetMimeType())
1600 1601 1602 ### Input methods. 1603
1604 - def Validate(self, value):
1605 1606 # The value should be an instance of 'Attachment', or 'None'. 1607 if value != None and not isinstance(value, attachment.Attachment): 1608 raise ValueError, \ 1609 "the value of an attachment field must be an 'Attachment'" 1610 return value
1611 1612
1613 - def ParseFormValue(self, request, name, attachment_stores):
1614 1615 encoding = request[name] 1616 # An empty string represnts a missing attachment, which is OK. 1617 if string.strip(encoding) == "": 1618 return None 1619 # The encoding is a semicolon-separated sequence indicating the 1620 # relevant information about the attachment. 1621 parts = string.split(encoding, ";") 1622 # Undo the URL encoding of each component. 1623 parts = map(urllib.unquote, parts) 1624 # Unpack the results. 1625 description, mime_type, location, file_name, store_id = parts 1626 # Figure out which AttachmentStore corresponds to the id 1627 # provided. 1628 store = attachment_stores[int(store_id)] 1629 # Create the attachment. 1630 value = attachment.Attachment(mime_type, description, 1631 file_name, location, 1632 store) 1633 return (self.Validate(value), 0)
1634 1635
1636 - def GetValueFromDomNode(self, node, attachment_store):
1637 1638 # Make sure 'node' is an "attachment" element. 1639 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1640 or node.tagName != "attachment": 1641 raise qm.QMException, \ 1642 qm.error("dom wrong tag for field", 1643 name=self.GetName(), 1644 right_tag="attachment", 1645 wrong_tag=node.tagName) 1646 return self.Validate(attachment.from_dom_node(node, attachment_store))
1647 1648 1649 ######################################################################## 1650
1651 -class ChoiceField(TextField):
1652 """A 'ChoiceField' allows choosing one of several values. 1653 1654 The set of acceptable values can be determined when the field is 1655 created or dynamically. The empty string is used as the "no 1656 choice" value, and cannot therefore be one of the permitted 1657 values.""" 1658
1659 - def GetItems(self):
1660 """Return the options from which to choose. 1661 1662 returns -- A sequence of strings, each of which will be 1663 presented as a choice for the user.""" 1664 1665 raise NotImplementedError
1666 1667
1668 - def FormatValueAsHtml(self, server, value, style, name = None):
1669 1670 if style not in ("new", "edit"): 1671 return qm.fields.TextField.FormatValueAsHtml(self, server, 1672 value, 1673 style, name) 1674 1675 # For an editable field, give the user a choice of available 1676 # resources. 1677 items = self.GetItems() 1678 if name is None: 1679 name = self.GetHtmlFormFieldName() 1680 result = '<select name="%s">\n' % name 1681 # HTML does not permit a "select" tag with no contained "option" 1682 # tags. Therefore, we ensure that there is always one option to 1683 # choose from. 1684 result += ' <option value="">--Select--</option>\n' 1685 # Add the choices for the ordinary options. 1686 for r in self.GetItems(): 1687 result += ' <option value="%s"' % r 1688 if r == value: 1689 result += ' selected="selected"' 1690 result += '>%s</option>\n' % r 1691 result += "</select>\n" 1692 1693 return result
1694 1695
1696 - def Validate(self, value):
1697 1698 value = super(ChoiceField, self).Validate(value) 1699 if value == "": 1700 raise ValueError, "No choice specified for %s." % self.GetTitle() 1701 return value
1702 1703 1704
1705 -class EnumerationField(ChoiceField):
1706 """A field that contains an enumeral value. 1707 1708 The enumeral value is selected from an enumerated set of values. 1709 An enumeral field uses the following properties: 1710 1711 enumeration -- A mapping from enumeral names to enumeral values. 1712 Names are converted to strings, and values are stored as integers. 1713 1714 ordered -- If non-zero, the enumerals are presented to the user 1715 ordered by value.""" 1716
1717 - def __init__(self, 1718 name = "", 1719 default_value=None, 1720 enumerals=[], 1721 **properties):
1722 """Create an enumeration field. 1723 1724 'enumerals' -- A sequence of strings of available 1725 enumerals. 1726 1727 'default_value' -- The default value for this enumeration. If 1728 'None', the first enumeral is used.""" 1729 1730 # If we're handed an encoded list of enumerals, decode it. 1731 if isinstance(enumerals, types.StringType): 1732 enumerals = string.split(enumerals, ",") 1733 # Make sure the default value is legitimate. 1734 if not default_value in enumerals and len(enumerals) > 0: 1735 default_value = enumerals[0] 1736 # Perform base class initialization. 1737 super(EnumerationField, self).__init__(name, default_value, 1738 **properties) 1739 # Remember the enumerals. 1740 self.__enumerals = enumerals
1741 1742
1743 - def GetItems(self):
1744 """Return a sequence of enumerals. 1745 1746 returns -- A sequence consisting of string enumerals objects, in 1747 the appropriate order.""" 1748 1749 return self.__enumerals
1750 1751
1752 - def GetHelp(self):
1753 enumerals = self.GetItems() 1754 help = """ 1755 An enumeration field. The value of this field must be one of a 1756 preselected set of enumerals. The enumerals for this field are, 1757 1758 """ 1759 for enumeral in enumerals: 1760 help = help + ' * "%s"\n\n' % enumeral 1761 help = help + ''' 1762 1763 The default value of this field is "%s". 1764 ''' % str(self.GetDefaultValue()) 1765 return help
1766 1767 ### Output methods. 1768
1769 - def MakeDomNodeForValue(self, value, document):
1770 1771 # Store the name of the enumeral. 1772 return xmlutil.create_dom_text_element(document, "enumeral", 1773 str(value))
1774 1775 1776 ### Input methods. 1777
1778 - def GetValueFromDomNode(self, node, attachment_store):
1779 1780 # Make sure 'node' is an '<enumeral>' element. 1781 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1782 or node.tagName != "enumeral": 1783 raise qm.QMException, \ 1784 qm.error("dom wrong tag for field", 1785 name=self.GetName(), 1786 right_tag="enumeral", 1787 wrong_tag=node.tagName) 1788 # Extract the value. 1789 return self.Validate(xmlutil.get_dom_text(node))
1790 1791 1792
1793 -class BooleanField(EnumerationField):
1794 """A field containing a boolean value. 1795 1796 The enumeration contains two values: true and false.""" 1797
1798 - def __init__(self, name = "", default_value = None, **properties):
1799 1800 # Construct the base class. 1801 EnumerationField.__init__(self, name, default_value, 1802 ["true", "false"], **properties)
1803 1804
1805 - def Validate(self, value):
1806 1807 if qm.common.parse_boolean(value): 1808 value = "true" 1809 else: 1810 value = "false" 1811 return super(BooleanField, self).Validate(value)
1812 1813 1814 ######################################################################## 1815
1816 -class TimeField(IntegerField):
1817 """A field containing a date and time. 1818 1819 The data and time is stored as seconds since the start of the UNIX 1820 epoch, UTC (the semantics of the standard 'time' function), with 1821 one-second precision. User representations of 'TimeField' fields 1822 show one-minue precision.""" 1823
1824 - def __init__(self, name = "", **properties):
1825 """Create a time field. 1826 1827 The field is given a default value for this field is 'None', which 1828 corresponds to the current time when the field value is first 1829 created.""" 1830 1831 # Perform base class initalization. 1832 super(TimeField, self).__init__(name, None, **properties)
1833 1834
1835 - def GetHelp(self):
1836 if time.daylight: 1837 time_zones = "%s or %s" % time.tzname 1838 else: 1839 time_zones = time.tzname[0] 1840 help = """ 1841 This field contains a time and date. The format for the 1842 time and date is 'YYYY-MM-DD HH:MM ZZZ'. The 'ZZZ' field is 1843 the time zone, and may be the local time zone (%s) or 1844 "UTC". 1845 1846 If the date component is omitted, today's date is used. If 1847 the time component is omitted, midnight is used. If the 1848 time zone component is omitted, the local time zone is 1849 used. 1850 """ % time_zones 1851 default_value = self.GetDefaultValue() 1852 if default_value is None: 1853 help = help + """ 1854 The default value for this field is the current time. 1855 """ 1856 else: 1857 help = help + """ 1858 The default value for this field is %s. 1859 """ % self.FormatValueAsText(default_value) 1860 return help
1861 1862 ### Output methods. 1863
1864 - def FormatValueAsText(self, value, columns=72):
1865 if value is None: 1866 return "now" 1867 else: 1868 return qm.common.format_time(value, local_time_zone=1)
1869 1870
1871 - def FormatValueAsHtml(self, server, value, style, name=None):
1872 1873 value = self.FormatValueAsText(value) 1874 1875 if style == "new" or style == "edit": 1876 return '<input type="text" size="8" name="%s" value="%s" />' \ 1877 % (name, value) 1878 elif style == "full" or style == "brief": 1879 # The time is formatted in three parts: the date, the time, 1880 # and the time zone. Replace the space between the time and 1881 # the time zone with a non-breaking space, so that if the 1882 # time is broken onto two lines, it is broken between the 1883 # date and the time. 1884 date, time, time_zone = string.split(value, " ") 1885 return date + " " + time + "&nbsp;" + time_zone 1886 elif style == "hidden": 1887 return '<input type="hidden" name="%s" value="%s" />' \ 1888 % (name, value) 1889 else: 1890 raise ValueError, style
1891 1892 ### Input methods. 1893
1894 - def ParseTextValue(self, value):
1895 1896 return self.Validate(qm.common.parse_time(value, 1897 default_local_time_zone=1))
1898 1899
1900 - def GetDefaultValue(self):
1901 1902 default_value = super(TimeField, self).GetDefaultValue() 1903 if default_value is not None: 1904 return default_value 1905 1906 return int(time.time())
1907 1908 1909
1910 -class PythonField(Field):
1911 """A 'PythonField' stores a Python value. 1912 1913 All 'PythonField's are computed; they are never written out, nor can 1914 they be specified directly by users. They are used in situations 1915 where the value of the field is specified programatically by the 1916 system.""" 1917
1918 - def __init__(self, name = "", default_value = None):
1919 1920 Field.__init__(self, name, default_value, computed = "true")
1921 1922 ######################################################################## 1923 # Local Variables: 1924 # mode: python 1925 # indent-tabs-mode: nil 1926 # fill-column: 72 1927 # End: 1928