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 # No mixed content ! 996 # We are only interested into element child-nodes. 997 children = [c for c in item.childNodes 998 if c.nodeType == xml.dom.Node.ELEMENT_NODE] 999 values[self.__key_field.GetValueFromDomNode 1000 (children[0], attachment_store)] =\ 1001 self.__value_field.GetValueFromDomNode(children[1], 1002 attachment_store) 1003 return self.Validate(values)
1004 1005 1006
1007 -class SetField(Field):
1008 """A field containing zero or more instances of some other field. 1009 1010 All contents must be of the same field type. A set field may not 1011 contain sets. 1012 1013 The default field value is set to an empty set.""" 1014
1015 - def __init__(self, contained, not_empty_set = "false", default_value = None, 1016 **properties):
1017 """Create a set field. 1018 1019 The name of the contained field is taken as the name of this 1020 field. 1021 1022 'contained' -- An 'Field' instance describing the 1023 elements of the set. 1024 1025 'not_empty_set' -- If true, this field may not be empty, 1026 i.e. the value of this field must contain at least one element. 1027 1028 raises -- 'ValueError' if 'contained' is a set field. 1029 1030 raises -- 'TypeError' if 'contained' is not a 'Field'.""" 1031 1032 if not properties.has_key('description'): 1033 properties['description'] = contained.GetDescription() 1034 1035 super(SetField, self).__init__( 1036 contained.GetName(), 1037 default_value or [], 1038 title = contained.GetTitle(), 1039 **properties) 1040 1041 # A set field may not contain a set field. 1042 if isinstance(contained, SetField): 1043 raise ValueError, \ 1044 "A set field may not contain a set field." 1045 if not isinstance(contained, Field): 1046 raise TypeError, "A set must contain another field." 1047 # Remeber the contained field type. 1048 self.__contained = contained 1049 self.__not_empty_set = not_empty_set == "true"
1050 1051
1052 - def GetHelp(self):
1053 return """ 1054 A set field. A set contains zero or more elements, all of the 1055 same type. The elements of the set are described below: 1056 1057 """ + self.__contained.GetHelp()
1058 1059
1060 - def GetSubfields(self):
1061 1062 return (self.__contained,)
1063 1064
1065 - def GetHtmlHelp(self, edit=0):
1066 help = Field.GetHtmlHelp(self) 1067 if edit: 1068 # In addition to the standard generated help, include 1069 # additional instructions about using the HTML controls. 1070 help = help + """ 1071 <hr noshade size="2"> 1072 <h4>Modifying This Field</h4> 1073 1074 <p>Add a new element to the set by clicking the 1075 <i>Add</i> button. The new element will have a default 1076 value until you change it. To remove elements from the 1077 set, select them by checking the boxes on the left side of 1078 the form. Then, click the <i>Remove</i> button.</p> 1079 """ 1080 return help
1081 1082 ### Output methods. 1083
1084 - def FormatValueAsText(self, value, columns=72):
1085 # If the set is empty, indicate this specially. 1086 if len(value) == 0: 1087 return "None" 1088 # Format each element of the set, and join them into a 1089 # comma-separated list. 1090 contained_field = self.__contained 1091 formatted_items = [] 1092 for item in value: 1093 formatted_item = contained_field.FormatValueAsText(item, columns) 1094 formatted_items.append(repr(formatted_item)) 1095 result = "[ " + string.join(formatted_items, ", ") + " ]" 1096 return qm.common.wrap_lines(result, columns)
1097 1098
1099 - def FormatValueAsHtml(self, server, value, style, name=None):
1100 # Use default value if requested. 1101 if value is None: 1102 value = [] 1103 # Use the default field form field name if requested. 1104 if name is None: 1105 name = self.GetHtmlFormFieldName() 1106 1107 contained_field = self.__contained 1108 if style == "brief" or style == "full": 1109 if len(value) == 0: 1110 # An empty set. 1111 return "None" 1112 formatted \ 1113 = map(lambda v: contained_field.FormatValueAsHtml(server, 1114 v, style), 1115 value) 1116 if style == "brief": 1117 # In the brief style, list elements separated by commas. 1118 separator = ", " 1119 else: 1120 # In the full style, list elements one per line. 1121 separator = "<br>\n" 1122 return string.join(formatted, separator) 1123 1124 elif style in ["new", "edit", "hidden"]: 1125 html = "" 1126 if value: 1127 # Create a table to represent the set -- but only if the set is 1128 # non-empty. A table with no body is invalid HTML. 1129 html += ('<table border="0" cellpadding="0" cellspacing="0">' 1130 "\n <tbody>\n") 1131 element_number = 0 1132 for element in value: 1133 html += " <tr>\n <td>" 1134 element_name = name + "_%d" % element_number 1135 checkbox_name = element_name + "_remove" 1136 if style == "edit": 1137 html += \ 1138 ('<input type="checkbox" name="%s" /></td>\n' 1139 ' <td>\n' 1140 % checkbox_name) 1141 html += contained_field.FormatValueAsHtml(server, 1142 element, 1143 style, 1144 element_name) 1145 html += " </td>\n </tr>\n" 1146 element_number += 1 1147 html += " </tbody>\n</table>\n" 1148 # The action field is used to keep track of whether the 1149 # "Add" or "Remove" button has been pushed. It would be 1150 # much nice if we could use JavaScript to update the 1151 # table, but Netscape 4, and even Mozilla 1.0, do not 1152 # permit that. Therefore, we have to go back to the server. 1153 html += '<input type="hidden" name="%s" value="" />' % name 1154 html += ('<input type="hidden" name="%s_count" value="%d" />' 1155 % (name, len(value))) 1156 if style != "hidden": 1157 html += ('<table border="0" cellpadding="0" cellspacing="0">\n' 1158 ' <tbody>\n' 1159 ' <tr>\n' 1160 ' <td><input type="button" name="%s_add" ' 1161 'value="Add Another" ' 1162 '''onclick="%s.value = 'add'; submit();" />''' 1163 '</td>\n' 1164 ' <td><input type="button" name="%s_remove"' 1165 'value="Remove Selected" ' 1166 '''onclick="%s.value = 'remove'; submit();" />''' 1167 '</td>\n' 1168 ' </tr>' 1169 ' </tbody>' 1170 '</table>' 1171 % (name, name, name, name)) 1172 return html
1173 1174
1175 - def MakeDomNodeForValue(self, value, document):
1176 1177 # Create a set element. 1178 element = document.createElement("set") 1179 # Add a child node for each item in the set. 1180 contained_field = self.__contained 1181 for item in value: 1182 # The contained field knows how to make a DOM node for each 1183 # item in the set. 1184 item_node = contained_field.MakeDomNodeForValue(item, document) 1185 element.appendChild(item_node) 1186 return element
1187 1188 ### Input methods. 1189
1190 - def Validate(self, value):
1191 1192 # If this field has the not_empty_set property set, make sure 1193 # the value complies. 1194 if self.__not_empty_set and len(value) == 0: 1195 raise ValueError, \ 1196 qm.error("empty set field value", 1197 field_title=self.GetTitle()) 1198 # Assume 'value' is a sequence. Copy it, simultaneously 1199 # validating each element in the contained field. 1200 return map(lambda v: self.__contained.Validate(v), 1201 value)
1202 1203
1204 - def ParseTextValue(self, value):
1205 1206 def invalid(tok): 1207 """Raise an exception indicating a problem with 'value'. 1208 1209 'tok' -- A token indicating the position of the problem. 1210 1211 This function does not return; instead, it raises an 1212 appropriate exception.""" 1213 1214 raise qm.QMException, \ 1215 qm.error("invalid set value", start = value[tok[2][1]:])
1216 1217 # Use the Python parser to handle the elements of the set. 1218 s = StringIO.StringIO(value) 1219 g = tokenize.generate_tokens(s.readline) 1220 1221 # Read the opening square bracket. 1222 tok = g.next() 1223 if tok[0] != tokenize.OP or tok[1] != "[": 1224 invalid(tok) 1225 1226 # There are no elements yet. 1227 elements = [] 1228 1229 # Keep going until we find the closing bracket. 1230 while 1: 1231 # If we've reached the closing bracket, the set is 1232 # complete. 1233 tok = g.next() 1234 if tok[0] == tokenize.OP and tok[1] == "]": 1235 break 1236 # If this is not the first element of the set, there should 1237 # be a comma before the next element. 1238 if elements: 1239 if tok[0] != tokenize.OP or tok[1] != ",": 1240 invalid(tok) 1241 tok = g.next() 1242 # The next token should be a string constant. 1243 if tok[0] != tokenize.STRING: 1244 invalid(tok) 1245 # Parse the string constant. 1246 v = eval(tok[1]) 1247 elements.append(self.__contained.ParseTextValue(v)) 1248 1249 # There should not be any tokens left over. 1250 tok = g.next() 1251 if not tokenize.ISEOF(tok[0]): 1252 invalid(tok) 1253 1254 return self.Validate(elements)
1255 1256
1257 - def ParseFormValue(self, request, name, attachment_stores):
1258 1259 values = [] 1260 redisplay = 0 1261 1262 # See if the user wants to add or remove elements from the set. 1263 action = request[name] 1264 # Loop over the entries for each of the elements, adding them to 1265 # the set. 1266 contained_field = self.__contained 1267 element = 0 1268 for element in xrange(int(request[name + "_count"])): 1269 element_name = name + "_%d" % element 1270 if not (action == "remove" 1271 and request.get(element_name + "_remove") == "on"): 1272 v, r = contained_field.ParseFormValue(request, 1273 element_name, 1274 attachment_stores) 1275 values.append(v) 1276 if r: 1277 redisplay = 1 1278 element += 1 1279 1280 # Remove entries from the request that might cause confusion 1281 # when the page is redisplayed. 1282 names = [] 1283 for n, v in request.items(): 1284 if n[:len(name)] == name: 1285 names.append(n) 1286 for n in names: 1287 del request[n] 1288 1289 # Validate the values. 1290 values = self.Validate(values) 1291 1292 # If the user requested another element, add to the set. 1293 if action == "add": 1294 redisplay = 1 1295 # There's no need to validate this new value and it may in 1296 # fact be dangerous to do so. For example, the default 1297 # value for a ChoiceField might be the "nothing selected" 1298 # value, which is not a valid selection. If the user does 1299 # not actually select something, the problem will be 1300 # reported when the form is submitted. 1301 values.append(contained_field.GetDefaultValue()) 1302 elif action == "remove": 1303 redisplay = 1 1304 1305 return (values, redisplay)
1306 1307
1308 - def GetValueFromDomNode(self, node, attachment_store):
1309 # Make sure 'node' is a '<set>' element. 1310 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1311 or node.tagName != "set": 1312 raise qm.QMException, \ 1313 qm.error("dom wrong tag for field", 1314 name=self.GetName(), 1315 right_tag="set", 1316 wrong_tag=node.tagName) 1317 # Use the contained field to extract values for the children of 1318 # this node, which are the set elements. 1319 contained_field = self.__contained 1320 fn = lambda n, f=contained_field, s=attachment_store: \ 1321 f.GetValueFromDomNode(n, s) 1322 values = map(fn, 1323 filter(lambda n: n.nodeType == xml.dom.Node.ELEMENT_NODE, 1324 node.childNodes)) 1325 return self.Validate(values)
1326 1327 1328 1329 ######################################################################## 1330
1331 -class UploadAttachmentPage(web.DtmlPage):
1332 """DTML context for generating upload-attachment.dtml.""" 1333
1334 - def __init__(self, 1335 attachment_store, 1336 field_name, 1337 encoding_name, 1338 summary_field_name, 1339 in_set=0):
1340 """Create a new page object. 1341 1342 'attachment_store' -- The AttachmentStore in which the new 1343 attachment will be placed. 1344 1345 'field_name' -- The user-visible name of the field for which an 1346 attachment is being uploaded. 1347 1348 'encoding_name' -- The name of the HTML input that should 1349 contain the encoded attachment. 1350 1351 'summary_field_name' -- The name of the HTML input that should 1352 contain the user-visible summary of the attachment. 1353 1354 'in_set' -- If true, the attachment is being added to an 1355 attachment set field.""" 1356 1357 web.DtmlPage.__init__(self, "attachment.dtml") 1358 # Use a brand-new location for the attachment data. 1359 self.location = attachment.make_temporary_location() 1360 # Set up properties. 1361 self.attachment_store_id = id(attachment_store) 1362 self.field_name = field_name 1363 self.encoding_name = encoding_name 1364 self.summary_field_name = summary_field_name 1365 self.in_set = in_set
1366 1367
1368 - def MakeSubmitUrl(self):
1369 """Return the URL for submitting this form.""" 1370 1371 return self.request.copy(AttachmentField.upload_url).AsUrl()
1372 1373 1374
1375 -class AttachmentField(Field):
1376 """A field containing a file attachment. 1377 1378 Note that the 'FormatValueAsHtml' method uses a popup upload form 1379 for uploading new attachment. The web server must be configured to 1380 handle the attachment submission requests. See 1381 'attachment.register_attachment_upload_script'.""" 1382 1383 upload_url = "/attachment-upload" 1384 """The URL used to upload data for an attachment. 1385 1386 The upload request will include these query arguments: 1387 1388 'location' -- The location at which to store the attachment data. 1389 1390 'file_data' -- The attachment data. 1391 1392 """ 1393 1394 download_url = "/attachment-download" 1395 """The URL used to download an attachment. 1396 1397 The download request will include this query argument: 1398 1399 'location' -- The location in the attachment store from which to 1400 retrieve the attachment data. 1401 1402 """ 1403 1404
1405 - def __init__(self, name = "", **properties):
1406 """Create an attachment field. 1407 1408 Sets the default value of the field to 'None'.""" 1409 1410 # Perform base class initialization. 1411 apply(Field.__init__, (self, name, None), properties)
1412 1413
1414 - def GetHelp(self):
1415 return """ 1416 An attachment field. An attachment consists of an uploaded 1417 file, which may be of any file type, plus a short description. 1418 The name of the file, as well as the file's MIME type, are also 1419 stored. The description is a single line of plain text. 1420 1421 An attachment need not be provided. The field may be left 1422 empty."""
1423 1424
1425 - def GetHtmlHelp(self, edit=0):
1426 help = Field.GetHtmlHelp(self) 1427 if edit: 1428 # In addition to the standard generated help, include 1429 # additional instructions about using the HTML controls. 1430 help = help + """ 1431 <hr noshade size="2"> 1432 <h4>Modifying This Field</h4> 1433 1434 <p>The text control describes the current value of this 1435 field, displaying the attachment's description, file name, 1436 and MIME type. If the field is empty, the text control 1437 displays "None". The text control cannot be edited.</p> 1438 1439 <p>To upload a new attachment (replacing the previous one, 1440 if any), click on the <i>Change...</i> button. To clear the 1441 current attachment and make the field empty, click on the 1442 <i>Clear</i> button.</p> 1443 """ 1444 return help
1445 1446 ### Output methods. 1447
1448 - def FormatValueAsText(self, value, columns=72):
1449 1450 return self._FormatSummary(value)
1451 1452
1453 - def FormatValueAsHtml(self, server, value, style, name=None):
1454 1455 field_name = self.GetName() 1456 1457 if value is None: 1458 # The attachment field value may be 'None', indicating no 1459 # attachment. 1460 pass 1461 elif isinstance(value, attachment.Attachment): 1462 location = value.GetLocation() 1463 mime_type = value.GetMimeType() 1464 description = value.GetDescription() 1465 file_name = value.GetFileName() 1466 else: 1467 raise ValueError, "'value' must be 'None' or an 'Attachment'" 1468 1469 # Use the default field form field name if requested. 1470 if name is None: 1471 name = self.GetHtmlFormFieldName() 1472 1473 if style == "full" or style == "brief": 1474 if value is None: 1475 return "None" 1476 # Link the attachment description to the data itself. 1477 download_url = web.WebRequest(self.download_url, 1478 location=location, 1479 mime_type=mime_type).AsUrl() 1480 # Here's a nice hack. If the user saves the attachment to a 1481 # file, browsers (some at least) guess the default file name 1482 # from the URL by taking everything following the final 1483 # slash character. So, we add this bogus-looking argument 1484 # to fool the browser into using our file name. 1485 download_url = download_url + \ 1486 "&=/" + urllib.quote_plus(file_name) 1487 1488 result = '<a href="%s">%s</a>' \ 1489 % (download_url, description) 1490 # For the full style, display the MIME type. 1491 if style == "full": 1492 result = result + ' (%s)' % (mime_type) 1493 return result 1494 1495 elif style == "new" or style == "edit": 1496 1497 # Some trickiness here. 1498 # 1499 # For attachment fields, the user specifies the file to 1500 # upload via a popup form, which is shown in a new browser 1501 # window. When that form is submitted, the attachment data 1502 # is immediately uploaded to the server. 1503 # 1504 # The information that's stored for an attachment is made of 1505 # four parts: a description, a MIME type, the file name, and 1506 # the location of the data itself. The user enters these 1507 # values in the popup form, which sets a hidden field on 1508 # this form to an encoding of that information. 1509 # 1510 # Also, when the popup form is submitted, the attachment 1511 # data is uploaded. By the time this form is submitted, the 1512 # attachment data should be uploaded already. The uploaded 1513 # attachment data is stored in the temporary attachment 1514 # area; it's copied into the IDB when the issue revision is 1515 # submitted. 1516 1517 summary_field_name = "_attachment" + name 1518 1519 # Fill in the description if there's already an attachment. 1520 summary_value = 'value="%s"' % self._FormatSummary(value) 1521 if value is None: 1522 field_value = "" 1523 else: 1524 # We'll encode all the relevant information. 1525 parts = (description, mime_type, location, file_name, 1526 str(id(value.GetStore()))) 1527 # Each part is URL-encoded. 1528 parts = map(urllib.quote, parts) 1529 # The parts are joined into a semicolon-delimited list. 1530 field_value = string.join(parts, ";") 1531 field_value = 'value="%s"' % field_value 1532 1533 # Generate the popup upload page. 1534 upload_page = \ 1535 UploadAttachmentPage(server.GetTemporaryAttachmentStore(), 1536 self.GetTitle(), 1537 name, 1538 summary_field_name)() 1539 1540 # Generate controls for this form. 1541 1542 # A text control for the user-visible summary of the 1543 # attachment. The "readonly" property isn't supported in 1544 # Netscape, so prevent the user from typing into the form by 1545 # forcing focus away from the control. 1546 text_control = ''' 1547 <input type="text" 1548 readonly 1549 size="40" 1550 name="%s" 1551 onfocus="this.blur();" 1552 %s>''' % (summary_field_name, summary_value) 1553 # A button to pop up the upload form. It causes the upload 1554 # page to appear in a popup window. 1555 upload_button \ 1556 = server.MakeButtonForCachedPopup("Upload", 1557 upload_page, 1558 window_width=640, 1559 window_height=320) 1560 # A button to clear the attachment. 1561 clear_button = ''' 1562 <input type="button" 1563 size="20" 1564 value=" Clear " 1565 name="_clear_%s" 1566 onclick="document.form.%s.value = 'None'; 1567 document.form.%s.value = '';" /> 1568 ''' % (field_name, summary_field_name, name) 1569 # A hidden control for the encoded attachment value. The 1570 # popup upload form fills in this control. 1571 hidden_control = ''' 1572 <input type="hidden" 1573 name="%s" 1574 %s>''' % (name, field_value) 1575 # Now assemble the controls with some layout bits. 1576 result = ''' 1577 %s%s<br> 1578 %s%s 1579 ''' % (text_control, hidden_control, upload_button, clear_button) 1580 1581 return result 1582 1583 else: 1584 raise ValueError, style
1585 1586
1587 - def MakeDomNodeForValue(self, value, document):
1588 return attachment.make_dom_node(value, document)
1589 1590
1591 - def _FormatSummary(self, attachment):
1592 """Generate a user-friendly summary for 'attachment'. 1593 1594 This value is used when generating the form. It can't be 1595 editied.""" 1596 1597 if attachment is None: 1598 return "None" 1599 else: 1600 return "%s (%s; %s)" \ 1601 % (attachment.GetDescription(), 1602 attachment.GetFileName(), 1603 attachment.GetMimeType())
1604 1605 1606 ### Input methods. 1607
1608 - def Validate(self, value):
1609 1610 # The value should be an instance of 'Attachment', or 'None'. 1611 if value != None and not isinstance(value, attachment.Attachment): 1612 raise ValueError, \ 1613 "the value of an attachment field must be an 'Attachment'" 1614 return value
1615 1616
1617 - def ParseFormValue(self, request, name, attachment_stores):
1618 1619 encoding = request[name] 1620 # An empty string represnts a missing attachment, which is OK. 1621 if string.strip(encoding) == "": 1622 return None 1623 # The encoding is a semicolon-separated sequence indicating the 1624 # relevant information about the attachment. 1625 parts = string.split(encoding, ";") 1626 # Undo the URL encoding of each component. 1627 parts = map(urllib.unquote, parts) 1628 # Unpack the results. 1629 description, mime_type, location, file_name, store_id = parts 1630 # Figure out which AttachmentStore corresponds to the id 1631 # provided. 1632 store = attachment_stores[int(store_id)] 1633 # Create the attachment. 1634 value = attachment.Attachment(mime_type, description, 1635 file_name, location, 1636 store) 1637 return (self.Validate(value), 0)
1638 1639
1640 - def GetValueFromDomNode(self, node, attachment_store):
1641 1642 # Make sure 'node' is an "attachment" element. 1643 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1644 or node.tagName != "attachment": 1645 raise qm.QMException, \ 1646 qm.error("dom wrong tag for field", 1647 name=self.GetName(), 1648 right_tag="attachment", 1649 wrong_tag=node.tagName) 1650 return self.Validate(attachment.from_dom_node(node, attachment_store))
1651 1652 1653 ######################################################################## 1654
1655 -class ChoiceField(TextField):
1656 """A 'ChoiceField' allows choosing one of several values. 1657 1658 The set of acceptable values can be determined when the field is 1659 created or dynamically. The empty string is used as the "no 1660 choice" value, and cannot therefore be one of the permitted 1661 values.""" 1662
1663 - def GetItems(self):
1664 """Return the options from which to choose. 1665 1666 returns -- A sequence of strings, each of which will be 1667 presented as a choice for the user.""" 1668 1669 raise NotImplementedError
1670 1671
1672 - def FormatValueAsHtml(self, server, value, style, name = None):
1673 1674 if style not in ("new", "edit"): 1675 return qm.fields.TextField.FormatValueAsHtml(self, server, 1676 value, 1677 style, name) 1678 1679 # For an editable field, give the user a choice of available 1680 # resources. 1681 items = self.GetItems() 1682 if name is None: 1683 name = self.GetHtmlFormFieldName() 1684 result = '<select name="%s">\n' % name 1685 # HTML does not permit a "select" tag with no contained "option" 1686 # tags. Therefore, we ensure that there is always one option to 1687 # choose from. 1688 result += ' <option value="">--Select--</option>\n' 1689 # Add the choices for the ordinary options. 1690 for r in self.GetItems(): 1691 result += ' <option value="%s"' % r 1692 if r == value: 1693 result += ' selected="selected"' 1694 result += '>%s</option>\n' % r 1695 result += "</select>\n" 1696 1697 return result
1698 1699
1700 - def Validate(self, value):
1701 1702 value = super(ChoiceField, self).Validate(value) 1703 if value == "": 1704 raise ValueError, "No choice specified for %s." % self.GetTitle() 1705 return value
1706 1707 1708
1709 -class EnumerationField(ChoiceField):
1710 """A field that contains an enumeral value. 1711 1712 The enumeral value is selected from an enumerated set of values. 1713 An enumeral field uses the following properties: 1714 1715 enumeration -- A mapping from enumeral names to enumeral values. 1716 Names are converted to strings, and values are stored as integers. 1717 1718 ordered -- If non-zero, the enumerals are presented to the user 1719 ordered by value.""" 1720
1721 - def __init__(self, 1722 name = "", 1723 default_value=None, 1724 enumerals=[], 1725 **properties):
1726 """Create an enumeration field. 1727 1728 'enumerals' -- A sequence of strings of available 1729 enumerals. 1730 1731 'default_value' -- The default value for this enumeration. If 1732 'None', the first enumeral is used.""" 1733 1734 # If we're handed an encoded list of enumerals, decode it. 1735 if isinstance(enumerals, types.StringType): 1736 enumerals = string.split(enumerals, ",") 1737 # Make sure the default value is legitimate. 1738 if not default_value in enumerals and len(enumerals) > 0: 1739 default_value = enumerals[0] 1740 # Perform base class initialization. 1741 super(EnumerationField, self).__init__(name, default_value, 1742 **properties) 1743 # Remember the enumerals. 1744 self.__enumerals = enumerals
1745 1746
1747 - def GetItems(self):
1748 """Return a sequence of enumerals. 1749 1750 returns -- A sequence consisting of string enumerals objects, in 1751 the appropriate order.""" 1752 1753 return self.__enumerals
1754 1755
1756 - def GetHelp(self):
1757 enumerals = self.GetItems() 1758 help = """ 1759 An enumeration field. The value of this field must be one of a 1760 preselected set of enumerals. The enumerals for this field are, 1761 1762 """ 1763 for enumeral in enumerals: 1764 help = help + ' * "%s"\n\n' % enumeral 1765 help = help + ''' 1766 1767 The default value of this field is "%s". 1768 ''' % str(self.GetDefaultValue()) 1769 return help
1770 1771 ### Output methods. 1772
1773 - def MakeDomNodeForValue(self, value, document):
1774 1775 # Store the name of the enumeral. 1776 return xmlutil.create_dom_text_element(document, "enumeral", 1777 str(value))
1778 1779 1780 ### Input methods. 1781
1782 - def GetValueFromDomNode(self, node, attachment_store):
1783 1784 # Make sure 'node' is an '<enumeral>' element. 1785 if node.nodeType != xml.dom.Node.ELEMENT_NODE \ 1786 or node.tagName != "enumeral": 1787 raise qm.QMException, \ 1788 qm.error("dom wrong tag for field", 1789 name=self.GetName(), 1790 right_tag="enumeral", 1791 wrong_tag=node.tagName) 1792 # Extract the value. 1793 return self.Validate(xmlutil.get_dom_text(node))
1794 1795 1796
1797 -class BooleanField(EnumerationField):
1798 """A field containing a boolean value. 1799 1800 The enumeration contains two values: true and false.""" 1801
1802 - def __init__(self, name = "", default_value = None, **properties):
1803 1804 # Construct the base class. 1805 EnumerationField.__init__(self, name, default_value, 1806 ["true", "false"], **properties)
1807 1808
1809 - def Validate(self, value):
1810 1811 if qm.common.parse_boolean(value): 1812 value = "true" 1813 else: 1814 value = "false" 1815 return super(BooleanField, self).Validate(value)
1816 1817 1818 ######################################################################## 1819
1820 -class TimeField(IntegerField):
1821 """A field containing a date and time. 1822 1823 The data and time is stored as seconds since the start of the UNIX 1824 epoch, UTC (the semantics of the standard 'time' function), with 1825 one-second precision. User representations of 'TimeField' fields 1826 show one-minue precision.""" 1827
1828 - def __init__(self, name = "", **properties):
1829 """Create a time field. 1830 1831 The field is given a default value for this field is 'None', which 1832 corresponds to the current time when the field value is first 1833 created.""" 1834 1835 # Perform base class initalization. 1836 super(TimeField, self).__init__(name, None, **properties)
1837 1838
1839 - def GetHelp(self):
1840 if time.daylight: 1841 time_zones = "%s or %s" % time.tzname 1842 else: 1843 time_zones = time.tzname[0] 1844 help = """ 1845 This field contains a time and date. The format for the 1846 time and date is 'YYYY-MM-DD HH:MM ZZZ'. The 'ZZZ' field is 1847 the time zone, and may be the local time zone (%s) or 1848 "UTC". 1849 1850 If the date component is omitted, today's date is used. If 1851 the time component is omitted, midnight is used. If the 1852 time zone component is omitted, the local time zone is 1853 used. 1854 """ % time_zones 1855 default_value = self.GetDefaultValue() 1856 if default_value is None: 1857 help = help + """ 1858 The default value for this field is the current time. 1859 """ 1860 else: 1861 help = help + """ 1862 The default value for this field is %s. 1863 """ % self.FormatValueAsText(default_value) 1864 return help
1865 1866 ### Output methods. 1867
1868 - def FormatValueAsText(self, value, columns=72):
1869 if value is None: 1870 return "now" 1871 else: 1872 return qm.common.format_time(value, local_time_zone=1)
1873 1874
1875 - def FormatValueAsHtml(self, server, value, style, name=None):
1876 1877 value = self.FormatValueAsText(value) 1878 1879 if style == "new" or style == "edit": 1880 return '<input type="text" size="8" name="%s" value="%s" />' \ 1881 % (name, value) 1882 elif style == "full" or style == "brief": 1883 # The time is formatted in three parts: the date, the time, 1884 # and the time zone. Replace the space between the time and 1885 # the time zone with a non-breaking space, so that if the 1886 # time is broken onto two lines, it is broken between the 1887 # date and the time. 1888 date, time, time_zone = string.split(value, " ") 1889 return date + " " + time + "&nbsp;" + time_zone 1890 elif style == "hidden": 1891 return '<input type="hidden" name="%s" value="%s" />' \ 1892 % (name, value) 1893 else: 1894 raise ValueError, style
1895 1896 ### Input methods. 1897
1898 - def ParseTextValue(self, value):
1899 1900 return self.Validate(qm.common.parse_time(value, 1901 default_local_time_zone=1))
1902 1903
1904 - def GetDefaultValue(self):
1905 1906 default_value = super(TimeField, self).GetDefaultValue() 1907 if default_value is not None: 1908 return default_value 1909 1910 return int(time.time())
1911 1912 1913
1914 -class PythonField(Field):
1915 """A 'PythonField' stores a Python value. 1916 1917 All 'PythonField's are computed; they are never written out, nor can 1918 they be specified directly by users. They are used in situations 1919 where the value of the field is specified programatically by the 1920 system.""" 1921
1922 - def __init__(self, name = "", default_value = None):
1923 1924 Field.__init__(self, name, default_value, computed = "true")
1925 1926 ######################################################################## 1927 # Local Variables: 1928 # mode: python 1929 # indent-tabs-mode: nil 1930 # fill-column: 72 1931 # End: 1932