1#!/usr/bin/env python3 2 3#****************************************************************************** 4# fieldformat.py, provides a class to handle field format types 5# 6# TreeLine, an information storage program 7# Copyright (C) 2020, Douglas W. Bell 8# 9# This is free software; you can redistribute it and/or modify it under the 10# terms of the GNU General Public License, either Version 2 or any later 11# version. This program is distributed in the hope that it will be useful, 12# but WITHOUT ANY WARRANTY. See the included LICENSE file for details. 13#****************************************************************************** 14 15import re 16import sys 17import enum 18import datetime 19import xml.sax.saxutils as saxutils 20import gennumber 21import genboolean 22import numbering 23import matheval 24import urltools 25import globalref 26 27fieldTypes = [N_('Text'), N_('HtmlText'), N_('OneLineText'), N_('SpacedText'), 28 N_('Number'), N_('Math'), N_('Numbering'), 29 N_('Date'), N_('Time'), N_('DateTime'), N_('Boolean'), 30 N_('Choice'), N_('AutoChoice'), N_('Combination'), 31 N_('AutoCombination'), N_('ExternalLink'), N_('InternalLink'), 32 N_('Picture'), N_('RegularExpression')] 33translatedFieldTypes = [_(name) for name in fieldTypes] 34_errorStr = '#####' 35_dateStampString = _('Now') 36_timeStampString = _('Now') 37MathResult = enum.Enum('MathResult', 'number date time boolean text') 38_mathResultBlank = {MathResult.number: 0, MathResult.date: 0, 39 MathResult.time: 0, MathResult.boolean: False, 40 MathResult.text: ''} 41_multipleSpaceRegEx = re.compile(r' {2,}') 42_lineBreakRegEx = re.compile(r'<br\s*/?>', re.I) 43_stripTagRe = re.compile(r'<.*?>') 44linkRegExp = re.compile(r'<a [^>]*href="([^"]+)"[^>]*>(.*?)</a>', re.I | re.S) 45linkSeparateNameRegExp = re.compile(r'(.*) \[(.*)\]\s*$') 46_imageRegExp = re.compile(r'<img [^>]*src="([^"]+)"[^>]*>', re.I | re.S) 47 48 49class TextField: 50 """Class to handle a rich-text field format type. 51 52 Stores options and format strings for a text field type. 53 Provides methods to return formatted data. 54 """ 55 typeName = 'Text' 56 defaultFormat = '' 57 showRichTextInCell = True 58 evalHtmlDefault = False 59 fixEvalHtmlSetting = True 60 defaultNumLines = 1 61 editorClassName = 'RichTextEditor' 62 sortTypeStr = '80_text' 63 supportsInitDefault = True 64 formatHelpMenuList = [] 65 def __init__(self, name, formatData=None): 66 """Initialize a field format type. 67 68 Arguments: 69 name -- the field name string 70 formatData -- the dict that defines this field's format 71 """ 72 self.name = name 73 if not formatData: 74 formatData = {} 75 self.prefix = formatData.get('prefix', '') 76 self.suffix = formatData.get('suffix', '') 77 self.initDefault = formatData.get('init', '') 78 self.numLines = formatData.get('lines', type(self).defaultNumLines) 79 self.sortKeyNum = formatData.get('sortkeynum', 0) 80 self.sortKeyForward = formatData.get('sortkeyfwd', True) 81 self.evalHtml = self.evalHtmlDefault 82 if not self.fixEvalHtmlSetting: 83 self.evalHtml = formatData.get('evalhtml', self.evalHtmlDefault) 84 self.useFileInfo = False 85 self.showInDialog = True 86 self.setFormat(formatData.get('format', type(self).defaultFormat)) 87 88 def formatData(self): 89 """Return a dictionary of this field's format settings. 90 """ 91 formatData = {'fieldname': self.name, 'fieldtype': self.typeName} 92 if self.format: 93 formatData['format'] = self.format 94 if self.prefix: 95 formatData['prefix'] = self.prefix 96 if self.suffix: 97 formatData['suffix'] = self.suffix 98 if self.initDefault: 99 formatData['init'] = self.initDefault 100 if self.numLines != self.defaultNumLines: 101 formatData['lines'] = self.numLines 102 if self.sortKeyNum > 0: 103 formatData['sortkeynum'] = self.sortKeyNum 104 if not self.sortKeyForward: 105 formatData['sortkeyfwd'] = False 106 if (not self.fixEvalHtmlSetting and 107 self.evalHtml != self.evalHtmlDefault): 108 formatData['evalhtml'] = self.evalHtml 109 return formatData 110 111 def setFormat(self, format): 112 """Set the format string and initialize as required. 113 114 Derived classes may raise a ValueError if the format is illegal. 115 Arguments: 116 format -- the new format string 117 """ 118 self.format = format 119 120 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 121 """Return formatted output text for this field in this node. 122 123 Arguments: 124 node -- the tree item storing the data 125 oneLine -- if True, returns only first line of output (for titles) 126 noHtml -- if True, removes all HTML markup (for titles, etc.) 127 formatHtml -- if False, escapes HTML from prefix & suffix 128 spotRef -- optional, used for ancestor field refs 129 """ 130 if self.useFileInfo and node.spotRefs: 131 # get file info node if not already the file info node 132 node = node.treeStructureRef().fileInfoNode 133 storedText = node.data.get(self.name, '') 134 if storedText: 135 return self.formatOutput(storedText, oneLine, noHtml, formatHtml) 136 return '' 137 138 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 139 """Return formatted output text from stored text for this field. 140 141 Arguments: 142 storedText -- the source text to format 143 oneLine -- if True, returns only first line of output (for titles) 144 noHtml -- if True, removes all HTML markup (for titles, etc.) 145 formatHtml -- if False, escapes HTML from prefix & suffix 146 """ 147 prefix = self.prefix 148 suffix = self.suffix 149 if oneLine: 150 storedText = _lineBreakRegEx.split(storedText, 1)[0] 151 if noHtml: 152 storedText = removeMarkup(storedText) 153 if formatHtml: 154 prefix = removeMarkup(prefix) 155 suffix = removeMarkup(suffix) 156 if not formatHtml: 157 prefix = saxutils.escape(prefix) 158 suffix = saxutils.escape(suffix) 159 return '{0}{1}{2}'.format(prefix, storedText, suffix) 160 161 def editorText(self, node): 162 """Return text formatted for use in the data editor. 163 164 The function for default text just returns the stored text. 165 Overloads may raise a ValueError if the data does not match the format. 166 Arguments: 167 node -- the tree item storing the data 168 """ 169 storedText = node.data.get(self.name, '') 170 return self.formatEditorText(storedText) 171 172 def formatEditorText(self, storedText): 173 """Return text formatted for use in the data editor. 174 175 The function for default text just returns the stored text. 176 Overloads may raise a ValueError if the data does not match the format. 177 Arguments: 178 storedText -- the source text to format 179 """ 180 return storedText 181 182 def storedText(self, editorText): 183 """Return new text to be stored based on text from the data editor. 184 185 The function for default text field just returns the editor text. 186 Overloads may raise a ValueError if the data does not match the format. 187 Arguments: 188 editorText -- the new text entered into the editor 189 """ 190 return editorText 191 192 def storedTextFromTitle(self, titleText): 193 """Return new text to be stored based on title text edits. 194 195 Overloads may raise a ValueError if the data does not match the format. 196 Arguments: 197 titleText -- the new title text 198 """ 199 return self.storedText(saxutils.escape(titleText)) 200 201 def getInitDefault(self): 202 """Return the initial stored value for newly created nodes. 203 """ 204 return self.initDefault 205 206 def setInitDefault(self, editorText): 207 """Set the default initial value from editor text. 208 209 The function for default text field just returns the stored text. 210 Arguments: 211 editorText -- the new text entered into the editor 212 """ 213 self.initDefault = self.storedText(editorText) 214 215 def getEditorInitDefault(self): 216 """Return initial value in editor format. 217 """ 218 value = '' 219 if self.supportsInitDefault: 220 try: 221 value = self.formatEditorText(self.initDefault) 222 except ValueError: 223 pass 224 return value 225 226 def initDefaultChoices(self): 227 """Return a list of choices for setting the init default. 228 """ 229 return [] 230 231 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 232 """Return a value to be used in math field equations. 233 234 Return None if blank and not zeroBlanks. 235 Arguments: 236 node -- the tree item storing the data 237 zeroBlanks -- accept blank field values if True 238 noMarkup -- if true, remove html markup 239 """ 240 storedText = node.data.get(self.name, '') 241 if storedText and noMarkup: 242 storedText = removeMarkup(storedText) 243 return storedText if storedText or zeroBlanks else None 244 245 def compareValue(self, node): 246 """Return a value for comparison to other nodes and for sorting. 247 248 Returns lowercase text for text fields or numbers for non-text fields. 249 Arguments: 250 node -- the tree item storing the data 251 """ 252 storedText = node.data.get(self.name, '') 253 return self.adjustedCompareValue(storedText) 254 255 def adjustedCompareValue(self, value): 256 """Return value adjusted like the compareValue for use in conditionals. 257 258 Text version removes any markup and goes to lower case. 259 Arguments: 260 value -- the comparison value to adjust 261 """ 262 value = removeMarkup(value) 263 return value.lower() 264 265 def sortKey(self, node): 266 """Return a tuple with field type and comparison value for sorting. 267 268 Allows different types to be sorted. 269 Arguments: 270 node -- the tree item storing the data 271 """ 272 return (self.sortTypeStr, self.compareValue(node)) 273 274 def changeType(self, newType): 275 """Change this field's type to newType with a default format. 276 277 Arguments: 278 newType -- the new type name, excluding "Field" 279 """ 280 self.__class__ = globals()[newType + 'Field'] 281 self.setFormat(self.defaultFormat) 282 if self.fixEvalHtmlSetting: 283 self.evalHtml = self.evalHtmlDefault 284 285 def sepName(self): 286 """Return the name enclosed with {* *} separators 287 """ 288 if self.useFileInfo: 289 return '{{*!{0}*}}'.format(self.name) 290 return '{{*{0}*}}'.format(self.name) 291 292 def getFormatHelpMenuList(self): 293 """Return the list of descriptions and keys for the format help menu. 294 """ 295 return self.formatHelpMenuList 296 297 298class HtmlTextField(TextField): 299 """Class to handle an HTML text field format type 300 301 Stores options and format strings for an HTML text field type. 302 Does not use the rich text editor. 303 Provides methods to return formatted data. 304 """ 305 typeName = 'HtmlText' 306 showRichTextInCell = False 307 evalHtmlDefault = True 308 editorClassName = 'HtmlTextEditor' 309 def __init__(self, name, formatData=None): 310 """Initialize a field format type. 311 312 Arguments: 313 name -- the field name string 314 formatData -- the dict that defines this field's format 315 """ 316 super().__init__(name, formatData) 317 318 def storedTextFromTitle(self, titleText): 319 """Return new text to be stored based on title text edits. 320 321 Overloads may raise a ValueError if the data does not match the format. 322 Arguments: 323 titleText -- the new title text 324 """ 325 return self.storedText(titleText) 326 327 328class OneLineTextField(TextField): 329 """Class to handle a single-line rich-text field format type. 330 331 Stores options and format strings for a text field type. 332 Provides methods to return formatted data. 333 """ 334 typeName = 'OneLineText' 335 editorClassName = 'OneLineTextEditor' 336 def __init__(self, name, formatData=None): 337 """Initialize a field format type. 338 339 Arguments: 340 name -- the field name string 341 formatData -- the dict that defines this field's format 342 """ 343 super().__init__(name, formatData) 344 345 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 346 """Return formatted output text from stored text for this field. 347 348 Arguments: 349 storedText -- the source text to format 350 oneLine -- if True, returns only first line of output (for titles) 351 noHtml -- if True, removes all HTML markup (for titles, etc.) 352 formatHtml -- if False, escapes HTML from prefix & suffix 353 """ 354 text = _lineBreakRegEx.split(storedText, 1)[0] 355 return super().formatOutput(text, oneLine, noHtml, formatHtml) 356 357 def formatEditorText(self, storedText): 358 """Return text formatted for use in the data editor. 359 360 Raises a ValueError if the data does not match the format. 361 Arguments: 362 storedText -- the source text to format 363 """ 364 return _lineBreakRegEx.split(storedText, 1)[0] 365 366 367class SpacedTextField(TextField): 368 """Class to handle a preformatted text field format type. 369 370 Stores options and format strings for a spaced text field type. 371 Uses <pre> tags to preserve spacing. 372 Does not use the rich text editor. 373 Provides methods to return formatted data. 374 """ 375 typeName = 'SpacedText' 376 showRichTextInCell = False 377 editorClassName = 'PlainTextEditor' 378 def __init__(self, name, formatData=None): 379 """Initialize a field format type. 380 381 Arguments: 382 name -- the field name string 383 formatData -- the dict that defines this field's format 384 """ 385 super().__init__(name, formatData) 386 387 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 388 """Return formatted output text from stored text for this field. 389 390 Arguments: 391 storedText -- the source text to format 392 oneLine -- if True, returns only first line of output (for titles) 393 noHtml -- if True, removes all HTML markup (for titles, etc.) 394 formatHtml -- if False, escapes HTML from prefix & suffix 395 """ 396 if storedText: 397 storedText = '<pre>{0}</pre>'.format(storedText) 398 return super().formatOutput(storedText, oneLine, noHtml, formatHtml) 399 400 def formatEditorText(self, storedText): 401 """Return text formatted for use in the data editor. 402 403 Arguments: 404 storedText -- the source text to format 405 """ 406 return saxutils.unescape(storedText) 407 408 def storedText(self, editorText): 409 """Return new text to be stored based on text from the data editor. 410 411 Arguments: 412 editorText -- the new text entered into the editor 413 """ 414 return saxutils.escape(editorText) 415 416 def storedTextFromTitle(self, titleText): 417 """Return new text to be stored based on title text edits. 418 419 Arguments: 420 titleText -- the new title text 421 """ 422 return self.storedText(titleText) 423 424 425class NumberField(HtmlTextField): 426 """Class to handle a general number field format type. 427 428 Stores options and format strings for a number field type. 429 Provides methods to return formatted data. 430 """ 431 typeName = 'Number' 432 defaultFormat = '#.##' 433 evalHtmlDefault = False 434 editorClassName = 'LineEditor' 435 sortTypeStr = '20_num' 436 formatHelpMenuList = [(_('Optional Digit\t#'), '#'), 437 (_('Required Digit\t0'), '0'), 438 (_('Digit or Space (external)\t<space>'), ' '), 439 ('', ''), 440 (_('Decimal Point\t.'), '.'), 441 (_('Decimal Comma\t,'), ','), 442 ('', ''), 443 (_('Comma Separator\t\\,'), '\\,'), 444 (_('Dot Separator\t\\.'), '\\.'), 445 (_('Space Separator (internal)\t<space>'), ' '), 446 ('', ''), 447 (_('Optional Sign\t-'), '-'), 448 (_('Required Sign\t+'), '+'), 449 ('', ''), 450 (_('Exponent (capital)\tE'), 'E'), 451 (_('Exponent (small)\te'), 'e')] 452 453 def __init__(self, name, formatData=None): 454 """Initialize a field format type. 455 456 Arguments: 457 name -- the field name string 458 formatData -- the dict that defines this field's format 459 """ 460 super().__init__(name, formatData) 461 462 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 463 """Return formatted output text from stored text for this field. 464 465 Arguments: 466 storedText -- the source text to format 467 oneLine -- if True, returns only first line of output (for titles) 468 noHtml -- if True, removes all HTML markup (for titles, etc.) 469 formatHtml -- if False, escapes HTML from prefix & suffix 470 """ 471 try: 472 text = gennumber.GenNumber(storedText).numStr(self.format) 473 except ValueError: 474 text = _errorStr 475 return super().formatOutput(text, oneLine, noHtml, formatHtml) 476 477 def formatEditorText(self, storedText): 478 """Return text formatted for use in the data editor. 479 480 Raises a ValueError if the data does not match the format. 481 Arguments: 482 storedText -- the source text to format 483 """ 484 if not storedText: 485 return '' 486 return gennumber.GenNumber(storedText).numStr(self.format) 487 488 def storedText(self, editorText): 489 """Return new text to be stored based on text from the data editor. 490 491 Raises a ValueError if the data does not match the format. 492 Arguments: 493 editorText -- the new text entered into the editor 494 """ 495 if not editorText: 496 return '' 497 return repr(gennumber.GenNumber().setFromStr(editorText, self.format)) 498 499 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 500 """Return a numeric value to be used in math field equations. 501 502 Return None if blank and not zeroBlanks, 503 raise a ValueError if it isn't a number. 504 Arguments: 505 node -- the tree item storing the data 506 zeroBlanks -- replace blank field values with zeros if True 507 noMarkup -- not applicable to numbers 508 """ 509 storedText = node.data.get(self.name, '') 510 if storedText: 511 return gennumber.GenNumber(storedText).num 512 return 0 if zeroBlanks else None 513 514 def adjustedCompareValue(self, value): 515 """Return value adjusted like the compareValue for use in conditionals. 516 517 Number version converts to a numeric value. 518 Arguments: 519 value -- the comparison value to adjust 520 """ 521 try: 522 return gennumber.GenNumber(value).num 523 except ValueError: 524 return 0 525 526 527class MathField(HtmlTextField): 528 """Class to handle a math calculation field type. 529 530 Stores options and format strings for a math field type. 531 Provides methods to return formatted data. 532 """ 533 typeName = 'Math' 534 defaultFormat = '#.##' 535 evalHtmlDefault = False 536 fixEvalHtmlSetting = False 537 editorClassName = 'ReadOnlyEditor' 538 def __init__(self, name, formatData=None): 539 """Initialize a field format type. 540 541 Arguments: 542 name -- the field name string 543 formatData -- the attributes that define this field's format 544 """ 545 super().__init__(name, formatData) 546 self.equation = None 547 self.resultType = MathResult[formatData.get('resulttype', 'number')] 548 equationText = formatData.get('eqn', '').strip() 549 if equationText: 550 self.equation = matheval.MathEquation(equationText) 551 try: 552 self.equation.validate() 553 except ValueError: 554 self.equation = None 555 556 def formatData(self): 557 """Return a dictionary of this field's attributes. 558 559 Add the math equation to the standard XML output. 560 """ 561 formatData = super().formatData() 562 if self.equation: 563 formatData['eqn'] = self.equation.equationText() 564 if self.resultType != MathResult.number: 565 formatData['resulttype'] = self.resultType.name 566 return formatData 567 568 def setFormat(self, format): 569 """Set the format string and initialize as required. 570 571 Arguments: 572 format -- the new format string 573 """ 574 if not hasattr(self, 'equation'): 575 self.equation = None 576 self.resultType = MathResult.number 577 super().setFormat(format) 578 579 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 580 """Return formatted output text from stored text for this field. 581 582 Arguments: 583 storedText -- the source text to format 584 oneLine -- if True, returns only first line of output (for titles) 585 noHtml -- if True, removes all HTML markup (for titles, etc.) 586 formatHtml -- if False, escapes HTML from prefix & suffix 587 """ 588 text = storedText 589 try: 590 if self.resultType == MathResult.number: 591 text = gennumber.GenNumber(text).numStr(self.format) 592 elif self.resultType == MathResult.date: 593 date = datetime.datetime.strptime(text, 594 DateField.isoFormat).date() 595 text = date.strftime(adjOutDateFormat(self.format)) 596 elif self.resultType == MathResult.time: 597 time = datetime.datetime.strptime(text, 598 TimeField.isoFormat).time() 599 text = time.strftime(adjOutDateFormat(self.format)) 600 elif self.resultType == MathResult.boolean: 601 text = genboolean.GenBoolean(text).boolStr(self.format) 602 except ValueError: 603 text = _errorStr 604 return super().formatOutput(text, oneLine, noHtml, formatHtml) 605 606 def formatEditorText(self, storedText): 607 """Return text formatted for use in the data editor. 608 609 Raises a ValueError if the data does not match the format. 610 Arguments: 611 storedText -- the source text to format 612 """ 613 if not storedText: 614 return '' 615 if self.resultType == MathResult.number: 616 return gennumber.GenNumber(storedText).numStr(self.format) 617 if self.resultType == MathResult.date: 618 date = datetime.datetime.strptime(storedText, 619 DateField.isoFormat).date() 620 editorFormat = adjOutDateFormat(globalref. 621 genOptions['EditDateFormat']) 622 return date.strftime(editorFormat) 623 if self.resultType == MathResult.time: 624 time = datetime.datetime.strptime(storedText, 625 TimeField.isoFormat).time() 626 editorFormat = adjOutDateFormat(globalref. 627 genOptions['EditTimeFormat']) 628 return time.strftime(editorFormat) 629 if self.resultType == MathResult.boolean: 630 return genboolean.GenBoolean(storedText).boolStr(self.format) 631 if storedText == _errorStr: 632 raise ValueError 633 return storedText 634 635 def equationText(self): 636 """Return the current equation text. 637 """ 638 if self.equation: 639 return self.equation.equationText() 640 return '' 641 642 def equationValue(self, node): 643 """Return a text value from the result of the equation. 644 645 Returns the '#####' error string for illegal math operations. 646 Arguments: 647 node -- the tree item with this equation 648 """ 649 if self.equation: 650 zeroValue = _mathResultBlank[self.resultType] 651 try: 652 num = self.equation.equationValue(node, self.resultType, 653 zeroValue, not self.evalHtml) 654 except ValueError: 655 return _errorStr 656 if num == None: 657 return '' 658 if self.resultType == MathResult.date: 659 date = DateField.refDate + datetime.timedelta(days=num) 660 return date.strftime(DateField.isoFormat) 661 if self.resultType == MathResult.time: 662 dateTime = datetime.datetime.combine(DateField.refDate, 663 TimeField.refTime) 664 dateTime = dateTime + datetime.timedelta(seconds=num) 665 time = dateTime.time() 666 return time.strftime(TimeField.isoFormat) 667 text = str(num) 668 if not self.evalHtml: 669 text = saxutils.escape(text) 670 return text 671 return '' 672 673 def resultClass(self): 674 """Return the result type's field class. 675 """ 676 return globals()[self.resultType.name.capitalize() + 'Field'] 677 678 def changeResultType(self, resultType): 679 """Change the result type and reset the output format. 680 681 Arguments: 682 resultType -- the new result type 683 """ 684 if resultType != self.resultType: 685 self.resultType = resultType 686 self.setFormat(self.resultClass().defaultFormat) 687 688 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 689 """Return a numeric value to be used in math field equations. 690 691 Return None if blank and not zeroBlanks, 692 raise a ValueError if it isn't valid. 693 Arguments: 694 node -- the tree item storing the data 695 zeroBlanks -- replace blank field values with zeros if True 696 noMarkup -- if true, remove html markup 697 """ 698 storedText = node.data.get(self.name, '') 699 if storedText: 700 if self.resultType == MathResult.number: 701 return gennumber.GenNumber(storedText).num 702 if self.resultType == MathResult.date: 703 date = datetime.datetime.strptime(storedText, 704 DateField.isoFormat).date() 705 return (date - DateField.refDate).days 706 if self.resultType == MathResult.time: 707 time = datetime.datetime.strptime(storedText, 708 TimeField.isoFormat).time() 709 return (time - TimeField.refTime).seconds 710 if self.resultType == MathResult.boolean: 711 return genboolean.GenBoolean(storedText).value 712 if noMarkup: 713 storedText = removeMarkup(storedText) 714 return storedText 715 return _mathResultBlank[self.resultType] if zeroBlanks else None 716 717 def adjustedCompareValue(self, value): 718 """Return value adjusted like the compareValue for use in conditionals. 719 720 Number version converts to a numeric value. 721 Arguments: 722 value -- the comparison value to adjust 723 """ 724 try: 725 if self.resultType == MathResult.number: 726 return gennumber.GenNumber(value).num 727 if self.resultType == MathResult.date: 728 date = datetime.datetime.strptime(value, 729 DateField.isoFormat).date() 730 return date.strftime(DateField.isoFormat) 731 if self.resultType == MathResult.time: 732 time = datetime.datetime.strptime(value, 733 TimeField.isoFormat).time() 734 return time.strftime(TimeField.isoFormat) 735 if self.resultType == MathResult.boolean: 736 return genboolean.GenBoolean(value).value 737 return value.lower() 738 except ValueError: 739 return 0 740 741 def sortKey(self, node): 742 """Return a tuple with field type and comparison value for sorting. 743 744 Allows different types to be sorted. 745 Arguments: 746 node -- the tree item storing the data 747 """ 748 return (self.resultClass().sortTypeStr, self.compareValue(node)) 749 750 def getFormatHelpMenuList(self): 751 """Return the list of descriptions and keys for the format help menu. 752 """ 753 return self.resultClass().formatHelpMenuList 754 755 756class NumberingField(HtmlTextField): 757 """Class to handle formats for hierarchical node numbering. 758 759 Stores options and format strings for a node numbering field type. 760 Provides methods to return formatted node numbers. 761 """ 762 typeName = 'Numbering' 763 defaultFormat = '1..' 764 evalHtmlDefault = False 765 editorClassName = 'LineEditor' 766 sortTypeStr = '10_numbering' 767 formatHelpMenuList = [(_('Number\t1'), '1'), 768 (_('Capital Letter\tA'), 'A'), 769 (_('Small Letter\ta'), 'a'), 770 (_('Capital Roman Numeral\tI'), 'I'), 771 (_('Small Roman Numeral\ti'), 'i'), 772 ('', ''), 773 (_('Level Separator\t/'), '/'), 774 (_('Section Separator\t.'), '.'), 775 ('', ''), 776 (_('"/" Character\t//'), '//'), 777 (_('"." Character\t..'), '..'), 778 ('', ''), 779 (_('Outline Example\tI../A../1../a)/i)'), 780 'I../A../1../a)/i)'), 781 (_('Section Example\t1.1.1.1'), '1.1.1.1')] 782 783 def __init__(self, name, formatData=None): 784 """Initialize a field format type. 785 786 Arguments: 787 name -- the field name string 788 formatData -- the attributes that define this field's format 789 """ 790 self.numFormat = None 791 super().__init__(name, formatData) 792 793 def setFormat(self, format): 794 """Set the format string and initialize as required. 795 796 Arguments: 797 format -- the new format string 798 """ 799 self.numFormat = numbering.NumberingGroup(format) 800 super().setFormat(format) 801 802 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 803 """Return formatted output text from stored text for this field. 804 805 Arguments: 806 storedText -- the source text to format 807 oneLine -- if True, returns only first line of output (for titles) 808 noHtml -- if True, removes all HTML markup (for titles, etc.) 809 formatHtml -- if False, escapes HTML from prefix & suffix 810 """ 811 try: 812 text = self.numFormat.numString(storedText) 813 except ValueError: 814 text = _errorStr 815 return super().formatOutput(text, oneLine, noHtml, formatHtml) 816 817 def formatEditorText(self, storedText): 818 """Return text formatted for use in the data editor. 819 820 Raises a ValueError if the data does not match the format. 821 Arguments: 822 storedText -- the source text to format 823 """ 824 if storedText: 825 checkData = [int(num) for num in storedText.split('.')] 826 return storedText 827 828 def storedText(self, editorText): 829 """Return new text to be stored based on text from the data editor. 830 831 Raises a ValueError if the data does not match the format. 832 Arguments: 833 editorText -- the new text entered into the editor 834 """ 835 if editorText: 836 checkData = [int(num) for num in editorText.split('.')] 837 return editorText 838 839 def adjustedCompareValue(self, value): 840 """Return value adjusted like the compareValue for use in conditionals. 841 842 Number version converts to a numeric value. 843 Arguments: 844 value -- the comparison value to adjust 845 """ 846 if value: 847 try: 848 return [int(num) for num in value.split('.')] 849 except ValueError: 850 pass 851 return [0] 852 853 854class DateField(HtmlTextField): 855 """Class to handle a general date field format type. 856 857 Stores options and format strings for a date field type. 858 Provides methods to return formatted data. 859 """ 860 typeName = 'Date' 861 defaultFormat = '%B %-d, %Y' 862 isoFormat = '%Y-%m-%d' 863 evalHtmlDefault = False 864 fixEvalHtmlSetting = False 865 editorClassName = 'DateEditor' 866 refDate = datetime.date(1970, 1, 1) 867 sortTypeStr = '40_date' 868 formatHelpMenuList = [(_('Day (1 or 2 digits)\t%-d'), '%-d'), 869 (_('Day (2 digits)\t%d'), '%d'), ('', ''), 870 (_('Weekday Abbreviation\t%a'), '%a'), 871 (_('Weekday Name\t%A'), '%A'), ('', ''), 872 (_('Month (1 or 2 digits)\t%-m'), '%-m'), 873 (_('Month (2 digits)\t%m'), '%m'), 874 (_('Month Abbreviation\t%b'), '%b'), 875 (_('Month Name\t%B'), '%B'), ('', ''), 876 (_('Year (2 digits)\t%y'), '%y'), 877 (_('Year (4 digits)\t%Y'), '%Y'), ('', ''), 878 (_('Week Number (0 to 53)\t%-U'), '%-U'), 879 (_('Day of year (1 to 366)\t%-j'), '%-j')] 880 def __init__(self, name, formatData=None): 881 """Initialize a field format type. 882 883 Arguments: 884 name -- the field name string 885 formatData -- the dict that defines this field's format 886 """ 887 super().__init__(name, formatData) 888 889 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 890 """Return formatted output text from stored text for this field. 891 892 Arguments: 893 storedText -- the source text to format 894 oneLine -- if True, returns only first line of output (for titles) 895 noHtml -- if True, removes all HTML markup (for titles, etc.) 896 formatHtml -- if False, escapes HTML from prefix & suffix 897 """ 898 try: 899 date = datetime.datetime.strptime(storedText, 900 DateField.isoFormat).date() 901 text = date.strftime(adjOutDateFormat(self.format)) 902 except ValueError: 903 text = _errorStr 904 if not self.evalHtml: 905 text = saxutils.escape(text) 906 return super().formatOutput(text, oneLine, noHtml, formatHtml) 907 908 def formatEditorText(self, storedText): 909 """Return text formatted for use in the data editor. 910 911 Raises a ValueError if the data does not match the format. 912 Arguments: 913 storedText -- the source text to format 914 """ 915 if not storedText: 916 return '' 917 date = datetime.datetime.strptime(storedText, 918 DateField.isoFormat).date() 919 editorFormat = adjOutDateFormat(globalref.genOptions['EditDateFormat']) 920 return date.strftime(editorFormat) 921 922 def storedText(self, editorText): 923 """Return new text to be stored based on text from the data editor. 924 925 Two digit years are interpretted as 1950-2049. 926 Raises a ValueError if the data does not match the format. 927 Arguments: 928 editorText -- the new text entered into the editor 929 """ 930 editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) 931 if not editorText: 932 return '' 933 editorFormat = adjInDateFormat(globalref.genOptions['EditDateFormat']) 934 try: 935 date = datetime.datetime.strptime(editorText, editorFormat).date() 936 except ValueError: # allow use of a 4-digit year to fix invalid dates 937 fullYearFormat = editorFormat.replace('%y', '%Y') 938 if fullYearFormat != editorFormat: 939 date = datetime.datetime.strptime(editorText, 940 fullYearFormat).date() 941 else: 942 raise 943 return date.strftime(DateField.isoFormat) 944 945 def getInitDefault(self): 946 """Return the initial stored value for newly created nodes. 947 """ 948 if self.initDefault == _dateStampString: 949 date = datetime.date.today() 950 return date.strftime(DateField.isoFormat) 951 return super().getInitDefault() 952 953 def setInitDefault(self, editorText): 954 """Set the default initial value from editor text. 955 956 The function for default text field just returns the stored text. 957 Arguments: 958 editorText -- the new text entered into the editor 959 """ 960 if editorText == _dateStampString: 961 self.initDefault = _dateStampString 962 else: 963 super().setInitDefault(editorText) 964 965 def getEditorInitDefault(self): 966 """Return initial value in editor format. 967 """ 968 if self.initDefault == _dateStampString: 969 return _dateStampString 970 return super().getEditorInitDefault() 971 972 def initDefaultChoices(self): 973 """Return a list of choices for setting the init default. 974 """ 975 return [_dateStampString] 976 977 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 978 """Return a numeric value to be used in math field equations. 979 980 Return None if blank and not zeroBlanks, 981 raise a ValueError if it isn't a valid date. 982 Arguments: 983 node -- the tree item storing the data 984 zeroBlanks -- replace blank field values with zeros if True 985 """ 986 storedText = node.data.get(self.name, '') 987 if storedText: 988 date = datetime.datetime.strptime(storedText, 989 DateField.isoFormat).date() 990 return (date - DateField.refDate).days 991 return 0 if zeroBlanks else None 992 993 def compareValue(self, node): 994 """Return a value for comparison to other nodes and for sorting. 995 996 Returns lowercase text for text fields or numbers for non-text fields. 997 Date field uses ISO date format (YYY-MM-DD). 998 Arguments: 999 node -- the tree item storing the data 1000 """ 1001 return node.data.get(self.name, '') 1002 1003 def adjustedCompareValue(self, value): 1004 """Return value adjusted like the compareValue for use in conditionals. 1005 1006 Date version converts to an ISO date format (YYYY-MM-DD). 1007 Arguments: 1008 value -- the comparison value to adjust 1009 """ 1010 value = _multipleSpaceRegEx.sub(' ', value.strip()) 1011 if not value: 1012 return '' 1013 if value == _dateStampString: 1014 date = datetime.date.today() 1015 return date.strftime(DateField.isoFormat) 1016 try: 1017 return self.storedText(value) 1018 except ValueError: 1019 return value 1020 1021 1022class TimeField(HtmlTextField): 1023 """Class to handle a general time field format type 1024 1025 Stores options and format strings for a time field type. 1026 Provides methods to return formatted data. 1027 """ 1028 typeName = 'Time' 1029 defaultFormat = '%-I:%M:%S %p' 1030 isoFormat = '%H:%M:%S.%f' 1031 evalHtmlDefault = False 1032 fixEvalHtmlSetting = False 1033 editorClassName = 'TimeEditor' 1034 numChoiceColumns = 2 1035 autoAddChoices = False 1036 refTime = datetime.time() 1037 sortTypeStr = '50_time' 1038 formatHelpMenuList = [(_('Hour (0-23, 1 or 2 digits)\t%-H'), '%-H'), 1039 (_('Hour (00-23, 2 digits)\t%H'), '%H'), 1040 (_('Hour (1-12, 1 or 2 digits)\t%-I'), '%-I'), 1041 (_('Hour (01-12, 2 digits)\t%I'), '%I'), ('', ''), 1042 (_('Minute (1 or 2 digits)\t%-M'), '%-M'), 1043 (_('Minute (2 digits)\t%M'), '%M'), ('', ''), 1044 (_('Second (1 or 2 digits)\t%-S'), '%-S'), 1045 (_('Second (2 digits)\t%S'), '%S'), ('', ''), 1046 (_('Microseconds (6 digits)\t%f'), '%f'), ('', ''), 1047 (_('AM/PM\t%p'), '%p')] 1048 def __init__(self, name, formatData=None): 1049 """Initialize a field format type. 1050 1051 Arguments: 1052 name -- the field name string 1053 formatData -- the attributes that define this field's format 1054 """ 1055 super().__init__(name, formatData) 1056 1057 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1058 """Return formatted output text from stored text for this field. 1059 1060 Arguments: 1061 storedText -- the source text to format 1062 oneLine -- if True, returns only first line of output (for titles) 1063 noHtml -- if True, removes all HTML markup (for titles, etc.) 1064 formatHtml -- if False, escapes HTML from prefix & suffix 1065 """ 1066 try: 1067 time = datetime.datetime.strptime(storedText, 1068 TimeField.isoFormat).time() 1069 outFormat = adjOutDateFormat(self.format) 1070 outFormat = adjTimeAmPm(outFormat, time) 1071 text = time.strftime(outFormat) 1072 except ValueError: 1073 text = _errorStr 1074 if not self.evalHtml: 1075 text = saxutils.escape(text) 1076 return super().formatOutput(text, oneLine, noHtml, formatHtml) 1077 1078 def formatEditorText(self, storedText): 1079 """Return text formatted for use in the data editor. 1080 1081 Raises a ValueError if the data does not match the format. 1082 Arguments: 1083 storedText -- the source text to format 1084 """ 1085 if not storedText: 1086 return '' 1087 time = datetime.datetime.strptime(storedText, 1088 TimeField.isoFormat).time() 1089 editorFormat = adjOutDateFormat(globalref.genOptions['EditTimeFormat']) 1090 editorFormat = adjTimeAmPm(editorFormat, time) 1091 return time.strftime(editorFormat) 1092 1093 def storedText(self, editorText): 1094 """Return new text to be stored based on text from the data editor. 1095 1096 Raises a ValueError if the data does not match the format. 1097 Arguments: 1098 editorText -- the new text entered into the editor 1099 """ 1100 editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) 1101 if not editorText: 1102 return '' 1103 editorFormat = adjInDateFormat(globalref.genOptions['EditTimeFormat']) 1104 time = None 1105 try: 1106 time = datetime.datetime.strptime(editorText, editorFormat).time() 1107 except ValueError: 1108 noSecFormat = editorFormat.replace(':%S', '') 1109 noSecFormat = _multipleSpaceRegEx.sub(' ', noSecFormat.strip()) 1110 try: 1111 time = datetime.datetime.strptime(editorText, 1112 noSecFormat).time() 1113 except ValueError: 1114 for altFormat in (editorFormat, noSecFormat): 1115 noAmFormat = altFormat.replace('%p', '') 1116 noAmFormat = _multipleSpaceRegEx.sub(' ', 1117 noAmFormat.strip()) 1118 try: 1119 time = datetime.datetime.strptime(editorText, 1120 noAmFormat).time() 1121 break 1122 except ValueError: 1123 pass 1124 if not time: 1125 raise ValueError 1126 return time.strftime(TimeField.isoFormat) 1127 1128 def annotatedComboChoices(self, editorText): 1129 """Return a list of (choice, annotation) tuples for the combo box. 1130 1131 Arguments: 1132 editorText -- the text entered into the editor 1133 """ 1134 editorFormat = adjOutDateFormat(globalref.genOptions['EditTimeFormat']) 1135 choices = [(datetime.datetime.now().time().strftime(editorFormat), 1136 '({0})'.format(_timeStampString))] 1137 for hour in (6, 9, 12, 15, 18, 21, 0): 1138 choices.append((datetime.time(hour).strftime(editorFormat), '')) 1139 return choices 1140 1141 def getInitDefault(self): 1142 """Return the initial stored value for newly created nodes. 1143 """ 1144 if self.initDefault == _timeStampString: 1145 time = datetime.datetime.now().time() 1146 return time.strftime(TimeField.isoFormat) 1147 return super().getInitDefault() 1148 1149 def setInitDefault(self, editorText): 1150 """Set the default initial value from editor text. 1151 1152 The function for default text field just returns the stored text. 1153 Arguments: 1154 editorText -- the new text entered into the editor 1155 """ 1156 if editorText == _timeStampString: 1157 self.initDefault = _timeStampString 1158 else: 1159 super().setInitDefault(editorText) 1160 1161 def getEditorInitDefault(self): 1162 """Return initial value in editor format. 1163 """ 1164 if self.initDefault == _timeStampString: 1165 return _timeStampString 1166 return super().getEditorInitDefault() 1167 1168 def initDefaultChoices(self): 1169 """Return a list of choices for setting the init default. 1170 """ 1171 return [_timeStampString] 1172 1173 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 1174 """Return a numeric value to be used in math field equations. 1175 1176 Return None if blank and not zeroBlanks, 1177 raise a ValueError if it isn't a valid time. 1178 Arguments: 1179 node -- the tree item storing the data 1180 zeroBlanks -- replace blank field values with zeros if True 1181 """ 1182 storedText = node.data.get(self.name, '') 1183 if storedText: 1184 time = datetime.datetime.strptime(storedText, 1185 TimeField.isoFormat).time() 1186 dateTime = datetime.datetime.combine(DateField.refDate, time) 1187 refDateTime = datetime.datetime.combine(DateField.refDate, 1188 TimeField.refTime) 1189 return (dateTime - refDateTime).seconds 1190 return 0 if zeroBlanks else None 1191 1192 def compareValue(self, node): 1193 """Return a value for comparison to other nodes and for sorting. 1194 1195 Returns lowercase text for text fields or numbers for non-text fields. 1196 Time field uses HH:MM:SS format. 1197 Arguments: 1198 node -- the tree item storing the data 1199 """ 1200 return node.data.get(self.name, '') 1201 1202 def adjustedCompareValue(self, value): 1203 """Return value adjusted like the compareValue for use in conditionals. 1204 1205 Time version converts to HH:MM:SS format. 1206 Arguments: 1207 value -- the comparison value to adjust 1208 """ 1209 value = _multipleSpaceRegEx.sub(' ', value.strip()) 1210 if not value: 1211 return '' 1212 if value == _timeStampString: 1213 time = datetime.datetime.now().time() 1214 return time.strftime(TimeField.isoFormat) 1215 try: 1216 return self.storedText(value) 1217 except ValueError: 1218 return value 1219 1220 1221class DateTimeField(HtmlTextField): 1222 """Class to handle a general date and time field format type. 1223 1224 Stores options and format strings for a date and time field type. 1225 Provides methods to return formatted data. 1226 """ 1227 typeName = 'DateTime' 1228 defaultFormat = '%B %-d, %Y %-I:%M:%S %p' 1229 isoFormat = '%Y-%m-%d %H:%M:%S.%f' 1230 evalHtmlDefault = False 1231 fixEvalHtmlSetting = False 1232 editorClassName = 'DateTimeEditor' 1233 refDateTime = datetime.datetime(1970, 1, 1) 1234 sortTypeStr ='45_datetime' 1235 formatHelpMenuList = [(_('Day (1 or 2 digits)\t%-d'), '%-d'), 1236 (_('Day (2 digits)\t%d'), '%d'), ('', ''), 1237 (_('Weekday Abbreviation\t%a'), '%a'), 1238 (_('Weekday Name\t%A'), '%A'), ('', ''), 1239 (_('Month (1 or 2 digits)\t%-m'), '%-m'), 1240 (_('Month (2 digits)\t%m'), '%m'), 1241 (_('Month Abbreviation\t%b'), '%b'), 1242 (_('Month Name\t%B'), '%B'), ('', ''), 1243 (_('Year (2 digits)\t%y'), '%y'), 1244 (_('Year (4 digits)\t%Y'), '%Y'), ('', ''), 1245 (_('Week Number (0 to 53)\t%-U'), '%-U'), 1246 (_('Day of year (1 to 366)\t%-j'), '%-j'), 1247 (_('Hour (0-23, 1 or 2 digits)\t%-H'), '%-H'), 1248 (_('Hour (00-23, 2 digits)\t%H'), '%H'), 1249 (_('Hour (1-12, 1 or 2 digits)\t%-I'), '%-I'), 1250 (_('Hour (01-12, 2 digits)\t%I'), '%I'), ('', ''), 1251 (_('Minute (1 or 2 digits)\t%-M'), '%-M'), 1252 (_('Minute (2 digits)\t%M'), '%M'), ('', ''), 1253 (_('Second (1 or 2 digits)\t%-S'), '%-S'), 1254 (_('Second (2 digits)\t%S'), '%S'), ('', ''), 1255 (_('Microseconds (6 digits)\t%f'), '%f'), ('', ''), 1256 (_('AM/PM\t%p'), '%p')] 1257 def __init__(self, name, formatData=None): 1258 """Initialize a field format type. 1259 1260 Arguments: 1261 name -- the field name string 1262 formatData -- the dict that defines this field's format 1263 """ 1264 super().__init__(name, formatData) 1265 1266 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1267 """Return formatted output text from stored text for this field. 1268 1269 Arguments: 1270 storedText -- the source text to format 1271 oneLine -- if True, returns only first line of output (for titles) 1272 noHtml -- if True, removes all HTML markup (for titles, etc.) 1273 formatHtml -- if False, escapes HTML from prefix & suffix 1274 """ 1275 try: 1276 dateTime = datetime.datetime.strptime(storedText, 1277 DateTimeField.isoFormat) 1278 outFormat = adjOutDateFormat(self.format) 1279 outFormat = adjTimeAmPm(outFormat, dateTime) 1280 text = dateTime.strftime(outFormat) 1281 except ValueError: 1282 text = _errorStr 1283 if not self.evalHtml: 1284 text = saxutils.escape(text) 1285 return super().formatOutput(text, oneLine, noHtml, formatHtml) 1286 1287 def formatEditorText(self, storedText): 1288 """Return text formatted for use in the data editor. 1289 1290 Raises a ValueError if the data does not match the format. 1291 Arguments: 1292 storedText -- the source text to format 1293 """ 1294 if not storedText: 1295 return '' 1296 dateTime = datetime.datetime.strptime(storedText, 1297 DateTimeField.isoFormat) 1298 editorFormat = '{0} {1}'.format(globalref.genOptions['EditDateFormat'], 1299 globalref.genOptions['EditTimeFormat']) 1300 editorFormat = adjOutDateFormat(editorFormat) 1301 editorFormat = adjTimeAmPm(editorFormat, dateTime) 1302 return dateTime.strftime(editorFormat) 1303 1304 def storedText(self, editorText): 1305 """Return new text to be stored based on text from the data editor. 1306 1307 Two digit years are interpretted as 1950-2049. 1308 Raises a ValueError if the data does not match the format. 1309 Arguments: 1310 editorText -- the new text entered into the editor 1311 """ 1312 editorText = _multipleSpaceRegEx.sub(' ', editorText.strip()) 1313 if not editorText: 1314 return '' 1315 editorFormat = '{0} {1}'.format(globalref.genOptions['EditDateFormat'], 1316 globalref.genOptions['EditTimeFormat']) 1317 editorFormat = adjInDateFormat(editorFormat) 1318 dateTime = None 1319 try: 1320 dateTime = datetime.datetime.strptime(editorText, editorFormat) 1321 except ValueError: 1322 noSecFormat = editorFormat.replace(':%S', '') 1323 noSecFormat = _multipleSpaceRegEx.sub(' ', noSecFormat.strip()) 1324 altFormats = [editorFormat, noSecFormat] 1325 for altFormat in altFormats[:]: 1326 noAmFormat = altFormat.replace('%p', '') 1327 noAmFormat = _multipleSpaceRegEx.sub(' ', noAmFormat.strip()) 1328 altFormats.append(noAmFormat) 1329 for altFormat in altFormats[:]: 1330 fullYearFormat = altFormat.replace('%y', '%Y') 1331 altFormats.append(fullYearFormat) 1332 for editorFormat in altFormats[1:]: 1333 try: 1334 dateTime = datetime.datetime.strptime(editorText, 1335 editorFormat) 1336 break 1337 except ValueError: 1338 pass 1339 if not dateTime: 1340 raise ValueError 1341 return dateTime.strftime(DateTimeField.isoFormat) 1342 1343 def getInitDefault(self): 1344 """Return the initial stored value for newly created nodes. 1345 """ 1346 if self.initDefault == _timeStampString: 1347 dateTime = datetime.datetime.now() 1348 return dateTime.strftime(DateTimeField.isoFormat) 1349 return super().getInitDefault() 1350 1351 def setInitDefault(self, editorText): 1352 """Set the default initial value from editor text. 1353 1354 The function for default text field just returns the stored text. 1355 Arguments: 1356 editorText -- the new text entered into the editor 1357 """ 1358 if editorText == _timeStampString: 1359 self.initDefault = _timeStampString 1360 else: 1361 super().setInitDefault(editorText) 1362 1363 def getEditorInitDefault(self): 1364 """Return initial value in editor format. 1365 """ 1366 if self.initDefault == _timeStampString: 1367 return _timeStampString 1368 return super().getEditorInitDefault() 1369 1370 def initDefaultChoices(self): 1371 """Return a list of choices for setting the init default. 1372 """ 1373 return [_timeStampString] 1374 1375 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 1376 """Return a numeric value to be used in math field equations. 1377 1378 Return None if blank and not zeroBlanks, 1379 raise a ValueError if it isn't a valid time. 1380 Arguments: 1381 node -- the tree item storing the data 1382 zeroBlanks -- replace blank field values with zeros if True 1383 """ 1384 storedText = node.data.get(self.name, '') 1385 if storedText: 1386 dateTime = datetime.datetime.strptime(storedText, 1387 DateTimeField.isoFormat) 1388 return (dateTime - DateTimeField.refDateTime).total_seconds() 1389 return 0 if zeroBlanks else None 1390 1391 def compareValue(self, node): 1392 """Return a value for comparison to other nodes and for sorting. 1393 1394 Returns lowercase text for text fields or numbers for non-text fields. 1395 DateTime field uses YYYY-MM-DD HH:MM:SS format. 1396 Arguments: 1397 node -- the tree item storing the data 1398 """ 1399 return node.data.get(self.name, '') 1400 1401 def adjustedCompareValue(self, value): 1402 """Return value adjusted like the compareValue for use in conditionals. 1403 1404 Time version converts to HH:MM:SS format. 1405 Arguments: 1406 value -- the comparison value to adjust 1407 """ 1408 value = _multipleSpaceRegEx.sub(' ', value.strip()) 1409 if not value: 1410 return '' 1411 if value == _timeStampString: 1412 dateTime = datetime.datetime.now() 1413 return dateTime.strftime(DateTimeField.isoFormat) 1414 try: 1415 return self.storedText(value) 1416 except ValueError: 1417 return value 1418 1419 1420class ChoiceField(HtmlTextField): 1421 """Class to handle a field with pre-defined, individual text choices. 1422 1423 Stores options and format strings for a choice field type. 1424 Provides methods to return formatted data. 1425 """ 1426 typeName = 'Choice' 1427 editSep = '/' 1428 defaultFormat = '1/2/3/4' 1429 evalHtmlDefault = False 1430 fixEvalHtmlSetting = False 1431 editorClassName = 'ComboEditor' 1432 numChoiceColumns = 1 1433 autoAddChoices = False 1434 formatHelpMenuList = [(_('Separator\t/'), '/'), ('', ''), 1435 (_('"/" Character\t//'), '//'), ('', ''), 1436 (_('Example\t1/2/3/4'), '1/2/3/4')] 1437 def __init__(self, name, formatData=None): 1438 """Initialize a field format type. 1439 1440 Arguments: 1441 name -- the field name string 1442 formatData -- the dict that defines this field's format 1443 """ 1444 super().__init__(name, formatData) 1445 1446 def setFormat(self, format): 1447 """Set the format string and initialize as required. 1448 1449 Arguments: 1450 format -- the new format string 1451 """ 1452 super().setFormat(format) 1453 self.choiceList = self.splitText(self.format) 1454 if self.evalHtml: 1455 self.choices = set(self.choiceList) 1456 else: 1457 self.choices = set([saxutils.escape(choice) for choice in 1458 self.choiceList]) 1459 1460 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1461 """Return formatted output text from stored text for this field. 1462 1463 Arguments: 1464 storedText -- the source text to format 1465 oneLine -- if True, returns only first line of output (for titles) 1466 noHtml -- if True, removes all HTML markup (for titles, etc.) 1467 formatHtml -- if False, escapes HTML from prefix & suffix 1468 """ 1469 if storedText not in self.choices: 1470 storedText = _errorStr 1471 return super().formatOutput(storedText, oneLine, noHtml, formatHtml) 1472 1473 def formatEditorText(self, storedText): 1474 """Return text formatted for use in the data editor. 1475 1476 Raises a ValueError if the data does not match the format. 1477 Arguments: 1478 storedText -- the source text to format 1479 """ 1480 if storedText and storedText not in self.choices: 1481 raise ValueError 1482 if self.evalHtml: 1483 return storedText 1484 return saxutils.unescape(storedText) 1485 1486 def storedText(self, editorText): 1487 """Return new text to be stored based on text from the data editor. 1488 1489 Raises a ValueError if the data does not match the format. 1490 Arguments: 1491 editorText -- the new text entered into the editor 1492 """ 1493 if not self.evalHtml: 1494 editorText = saxutils.escape(editorText) 1495 if not editorText or editorText in self.choices: 1496 return editorText 1497 raise ValueError 1498 1499 def comboChoices(self): 1500 """Return a list of choices for the combo box. 1501 """ 1502 return self.choiceList 1503 1504 def initDefaultChoices(self): 1505 """Return a list of choices for setting the init default. 1506 """ 1507 return self.choiceList 1508 1509 def splitText(self, textStr): 1510 """Split textStr using editSep, return a list of strings. 1511 1512 Double editSep's are not split (become single). 1513 Removes duplicates and empty strings. 1514 Arguments: 1515 textStr -- the text to split 1516 """ 1517 result = [] 1518 textStr = textStr.replace(self.editSep * 2, '\0') 1519 for text in textStr.split(self.editSep): 1520 text = text.strip().replace('\0', self.editSep) 1521 if text and text not in result: 1522 result.append(text) 1523 return result 1524 1525 1526class AutoChoiceField(HtmlTextField): 1527 """Class to handle a field with automatically populated text choices. 1528 1529 Stores options and possible entries for an auto-choice field type. 1530 Provides methods to return formatted data. 1531 """ 1532 typeName = 'AutoChoice' 1533 evalHtmlDefault = False 1534 fixEvalHtmlSetting = False 1535 editorClassName = 'ComboEditor' 1536 numChoiceColumns = 1 1537 autoAddChoices = True 1538 def __init__(self, name, formatData=None): 1539 """Initialize a field format type. 1540 1541 Arguments: 1542 name -- the field name string 1543 formatData -- the attributes that define this field's format 1544 """ 1545 super().__init__(name, formatData) 1546 self.choices = set() 1547 1548 def formatEditorText(self, storedText): 1549 """Return text formatted for use in the data editor. 1550 1551 Arguments: 1552 storedText -- the source text to format 1553 """ 1554 if self.evalHtml: 1555 return storedText 1556 return saxutils.unescape(storedText) 1557 1558 def storedText(self, editorText): 1559 """Return new text to be stored based on text from the data editor. 1560 1561 Arguments: 1562 editorText -- the new text entered into the editor 1563 """ 1564 if self.evalHtml: 1565 return editorText 1566 return saxutils.escape(editorText) 1567 1568 def comboChoices(self): 1569 """Return a list of choices for the combo box. 1570 """ 1571 if self.evalHtml: 1572 choices = self.choices 1573 else: 1574 choices = [saxutils.unescape(text) for text in 1575 self.choices] 1576 return sorted(choices, key=str.lower) 1577 1578 def addChoice(self, text): 1579 """Add a new choice. 1580 1581 Arguments: 1582 text -- the choice to be added 1583 """ 1584 if text: 1585 self.choices.add(text) 1586 1587 def clearChoices(self): 1588 """Remove all current choices. 1589 """ 1590 self.choices = set() 1591 1592 1593class CombinationField(ChoiceField): 1594 """Class to handle a field with multiple pre-defined text choices. 1595 1596 Stores options and format strings for a combination field type. 1597 Provides methods to return formatted data. 1598 """ 1599 typeName = 'Combination' 1600 editorClassName = 'CombinationEditor' 1601 numChoiceColumns = 2 1602 def __init__(self, name, formatData=None): 1603 """Initialize a field format type. 1604 1605 Arguments: 1606 name -- the field name string 1607 formatData -- the dict that defines this field's format 1608 """ 1609 super().__init__(name, formatData) 1610 1611 def setFormat(self, format): 1612 """Set the format string and initialize as required. 1613 1614 Arguments: 1615 format -- the new format string 1616 """ 1617 TextField.setFormat(self, format) 1618 if not self.evalHtml: 1619 format = saxutils.escape(format) 1620 self.choiceList = self.splitText(format) 1621 self.choices = set(self.choiceList) 1622 self.outputSep = '' 1623 1624 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 1625 """Return formatted output text for this field in this node. 1626 1627 Sets output separator prior to calling base class methods. 1628 Arguments: 1629 node -- the tree item storing the data 1630 oneLine -- if True, returns only first line of output (for titles) 1631 noHtml -- if True, removes all HTML markup (for titles, etc.) 1632 formatHtml -- if False, escapes HTML from prefix & suffix 1633 spotRef -- optional, used for ancestor field refs 1634 """ 1635 self.outputSep = node.formatRef.outputSeparator 1636 return super().outputText(node, oneLine, noHtml, formatHtml, spotRef) 1637 1638 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1639 """Return formatted output text from stored text for this field. 1640 1641 Arguments: 1642 storedText -- the source text to format 1643 oneLine -- if True, returns only first line of output (for titles) 1644 noHtml -- if True, removes all HTML markup (for titles, etc.) 1645 formatHtml -- if False, escapes HTML from prefix & suffix 1646 """ 1647 selections, valid = self.sortedSelections(storedText) 1648 if valid: 1649 result = self.outputSep.join(selections) 1650 else: 1651 result = _errorStr 1652 return TextField.formatOutput(self, result, oneLine, noHtml, 1653 formatHtml) 1654 1655 def formatEditorText(self, storedText): 1656 """Return text formatted for use in the data editor. 1657 1658 Raises a ValueError if the data does not match the format. 1659 Arguments: 1660 storedText -- the source text to format 1661 """ 1662 selections = set(self.splitText(storedText)) 1663 if selections.issubset(self.choices): 1664 if self.evalHtml: 1665 return storedText 1666 return saxutils.unescape(storedText) 1667 raise ValueError 1668 1669 def storedText(self, editorText): 1670 """Return new text to be stored based on text from the data editor. 1671 1672 Raises a ValueError if the data does not match the format. 1673 Arguments: 1674 editorText -- the new text entered into the editor 1675 """ 1676 if not self.evalHtml: 1677 editorText = saxutils.escape(editorText) 1678 selections, valid = self.sortedSelections(editorText) 1679 if not valid: 1680 raise ValueError 1681 return self.joinText(selections) 1682 1683 def comboChoices(self): 1684 """Return a list of choices for the combo box. 1685 """ 1686 if self.evalHtml: 1687 return self.choiceList 1688 return [saxutils.unescape(text) for text in self.choiceList] 1689 1690 def comboActiveChoices(self, editorText): 1691 """Return a sorted list of choices currently in editorText. 1692 1693 Arguments: 1694 editorText -- the text entered into the editor 1695 """ 1696 selections, valid = self.sortedSelections(saxutils.escape(editorText)) 1697 if self.evalHtml: 1698 return selections 1699 return [saxutils.unescape(text) for text in selections] 1700 1701 def initDefaultChoices(self): 1702 """Return a list of choices for setting the init default. 1703 """ 1704 return [] 1705 1706 def sortedSelections(self, inText): 1707 """Split inText using editSep and sort like format string. 1708 1709 Return a tuple of resulting selection list and bool validity. 1710 Valid if all choices are in the format string. 1711 Arguments: 1712 inText -- the text to split and sequence 1713 """ 1714 selections = set(self.splitText(inText)) 1715 result = [text for text in self.choiceList if text in selections] 1716 return (result, len(selections) == len(result)) 1717 1718 def joinText(self, textList): 1719 """Join the text list using editSep, return the string. 1720 1721 Any editSep in text items become double. 1722 Arguments: 1723 textList -- the list of text items to join 1724 """ 1725 return self.editSep.join([text.replace(self.editSep, self.editSep * 2) 1726 for text in textList]) 1727 1728 1729class AutoCombinationField(CombinationField): 1730 """Class for a field with multiple automatically populated text choices. 1731 1732 Stores options and possible entries for an auto-choice field type. 1733 Provides methods to return formatted data. 1734 """ 1735 typeName = 'AutoCombination' 1736 autoAddChoices = True 1737 defaultFormat = '' 1738 formatHelpMenuList = [] 1739 def __init__(self, name, formatData=None): 1740 """Initialize a field format type. 1741 1742 Arguments: 1743 name -- the field name string 1744 formatData -- the attributes that define this field's format 1745 """ 1746 super().__init__(name, formatData) 1747 self.choices = set() 1748 self.outputSep = '' 1749 1750 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 1751 """Return formatted output text for this field in this node. 1752 1753 Sets output separator prior to calling base class methods. 1754 Arguments: 1755 node -- the tree item storing the data 1756 oneLine -- if True, returns only first line of output (for titles) 1757 noHtml -- if True, removes all HTML markup (for titles, etc.) 1758 formatHtml -- if False, escapes HTML from prefix & suffix 1759 spotRef -- optional, used for ancestor field refs 1760 """ 1761 self.outputSep = node.formatRef.outputSeparator 1762 return super().outputText(node, oneLine, noHtml, formatHtml, spotRef) 1763 1764 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1765 """Return formatted output text from stored text for this field. 1766 1767 Arguments: 1768 storedText -- the source text to format 1769 oneLine -- if True, returns only first line of output (for titles) 1770 noHtml -- if True, removes all HTML markup (for titles, etc.) 1771 formatHtml -- if False, escapes HTML from prefix & suffix 1772 """ 1773 result = self.outputSep.join(self.splitText(storedText)) 1774 return TextField.formatOutput(self, result, oneLine, noHtml, 1775 formatHtml) 1776 1777 def formatEditorText(self, storedText): 1778 """Return text formatted for use in the data editor. 1779 1780 Arguments: 1781 storedText -- the source text to format 1782 """ 1783 if self.evalHtml: 1784 return storedText 1785 return saxutils.unescape(storedText) 1786 1787 def storedText(self, editorText): 1788 """Return new text to be stored based on text from the data editor. 1789 1790 Also resets outputSep, to be defined at the next output. 1791 Arguments: 1792 editorText -- the new text entered into the editor 1793 """ 1794 self.outputSep = '' 1795 if not self.evalHtml: 1796 editorText = saxutils.escape(editorText) 1797 selections = sorted(self.splitText(editorText), key=str.lower) 1798 return self.joinText(selections) 1799 1800 def comboChoices(self): 1801 """Return a list of choices for the combo box. 1802 """ 1803 if self.evalHtml: 1804 choices = self.choices 1805 else: 1806 choices = [saxutils.unescape(text) for text in 1807 self.choices] 1808 return sorted(choices, key=str.lower) 1809 1810 def comboActiveChoices(self, editorText): 1811 """Return a sorted list of choices currently in editorText. 1812 1813 Arguments: 1814 editorText -- the text entered into the editor 1815 """ 1816 selections, valid = self.sortedSelections(saxutils.escape(editorText)) 1817 if self.evalHtml: 1818 return selections 1819 return [saxutils.unescape(text) for text in selections] 1820 1821 def sortedSelections(self, inText): 1822 """Split inText using editSep and sort like format string. 1823 1824 Return a tuple of resulting selection list and bool validity. 1825 This version always returns valid. 1826 Arguments: 1827 inText -- the text to split and sequence 1828 """ 1829 selections = sorted(self.splitText(inText), key=str.lower) 1830 return (selections, True) 1831 1832 def addChoice(self, text): 1833 """Add a new choice. 1834 1835 Arguments: 1836 text -- the stored text combinations to be added 1837 """ 1838 for choice in self.splitText(text): 1839 self.choices.add(choice) 1840 1841 def clearChoices(self): 1842 """Remove all current choices. 1843 """ 1844 self.choices = set() 1845 1846 1847class BooleanField(ChoiceField): 1848 """Class to handle a general boolean field format type. 1849 1850 Stores options and format strings for a boolean field type. 1851 Provides methods to return formatted data. 1852 """ 1853 typeName = 'Boolean' 1854 defaultFormat = _('yes/no') 1855 evalHtmlDefault = False 1856 fixEvalHtmlSetting = False 1857 sortTypeStr ='30_bool' 1858 formatHelpMenuList = [(_('true/false'), 'true/false'), 1859 (_('T/F'), 'T/F'), ('', ''), 1860 (_('yes/no'), 'yes/no'), 1861 (_('Y/N'), 'Y/N'), ('', ''), 1862 ('1/0', '1/0')] 1863 def __init__(self, name, formatData=None): 1864 """Initialize a field format type. 1865 1866 Arguments: 1867 name -- the field name string 1868 formatData -- the dict that defines this field's format 1869 """ 1870 super().__init__(name, formatData) 1871 1872 def setFormat(self, format): 1873 """Set the format string and initialize as required. 1874 1875 Arguments: 1876 format -- the new format string 1877 """ 1878 HtmlTextField.setFormat(self, format) 1879 self.strippedFormat = removeMarkup(self.format) 1880 1881 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 1882 """Return formatted output text from stored text for this field. 1883 1884 Arguments: 1885 storedText -- the source text to format 1886 oneLine -- if True, returns only first line of output (for titles) 1887 noHtml -- if True, removes all HTML markup (for titles, etc.) 1888 formatHtml -- if False, escapes HTML from prefix & suffix 1889 """ 1890 try: 1891 text = genboolean.GenBoolean(storedText).boolStr(self.format) 1892 except ValueError: 1893 text = _errorStr 1894 if not self.evalHtml: 1895 text = saxutils.escape(text) 1896 return HtmlTextField.formatOutput(self, text, oneLine, noHtml, 1897 formatHtml) 1898 1899 def formatEditorText(self, storedText): 1900 """Return text formatted for use in the data editor. 1901 1902 Raises a ValueError if the data does not match the format. 1903 Arguments: 1904 storedText -- the source text to format 1905 """ 1906 if not storedText: 1907 return '' 1908 boolFormat = self.strippedFormat if self.evalHtml else self.format 1909 return genboolean.GenBoolean(storedText).boolStr(boolFormat) 1910 1911 def storedText(self, editorText): 1912 """Return new text to be stored based on text from the data editor. 1913 1914 Raises a ValueError if the data does not match the format. 1915 Arguments: 1916 editorText -- the new text entered into the editor 1917 """ 1918 if not editorText: 1919 return '' 1920 boolFormat = self.strippedFormat if self.evalHtml else self.format 1921 try: 1922 return repr(genboolean.GenBoolean().setFromStr(editorText, 1923 boolFormat)) 1924 except ValueError: 1925 return repr(genboolean.GenBoolean(editorText)) 1926 1927 def comboChoices(self): 1928 """Return a list of choices for the combo box. 1929 """ 1930 if self.evalHtml: 1931 return self.splitText(self.strippedFormat) 1932 return self.splitText(self.format) 1933 1934 def initDefaultChoices(self): 1935 """Return a list of choices for setting the init default. 1936 """ 1937 return self.comboChoices() 1938 1939 def mathValue(self, node, zeroBlanks=True, noMarkup=True): 1940 """Return a value to be used in math field equations. 1941 1942 Return None if blank and not zeroBlanks, 1943 raise a ValueError if it isn't a valid boolean. 1944 Arguments: 1945 node -- the tree item storing the data 1946 zeroBlanks -- replace blank field values with zeros if True 1947 """ 1948 storedText = node.data.get(self.name, '') 1949 if storedText: 1950 return genboolean.GenBoolean(storedText).value 1951 return False if zeroBlanks else None 1952 1953 def compareValue(self, node): 1954 """Return a value for comparison to other nodes and for sorting. 1955 1956 Returns lowercase text for text fields or numbers for non-text fields. 1957 Bool fields return True or False values. 1958 Arguments: 1959 node -- the tree item storing the data 1960 """ 1961 storedText = node.data.get(self.name, '') 1962 try: 1963 return genboolean.GenBoolean(storedText).value 1964 except ValueError: 1965 return False 1966 1967 def adjustedCompareValue(self, value): 1968 """Return value adjusted like the compareValue for use in conditionals. 1969 1970 Bool version converts to a bool value. 1971 Arguments: 1972 value -- the comparison value to adjust 1973 """ 1974 try: 1975 return genboolean.GenBoolean().setFromStr(value, self.format).value 1976 except ValueError: 1977 try: 1978 return genboolean.GenBoolean(value).value 1979 except ValueError: 1980 return False 1981 1982 1983class ExternalLinkField(HtmlTextField): 1984 """Class to handle a field containing various types of external HTML links. 1985 1986 Protocol choices include http, https, file, mailto. 1987 Stores data as HTML tags, shows in editors as "protocol:address [name]". 1988 """ 1989 typeName = 'ExternalLink' 1990 evalHtmlDefault = False 1991 editorClassName = 'ExtLinkEditor' 1992 sortTypeStr ='60_link' 1993 1994 def __init__(self, name, formatData=None): 1995 """Initialize a field format type. 1996 1997 Arguments: 1998 name -- the field name string 1999 formatData -- the attributes that define this field's format 2000 """ 2001 super().__init__(name, formatData) 2002 2003 def addressAndName(self, storedText): 2004 """Return the link title and the name from the given stored link. 2005 2006 Raise ValueError if the stored text is not formatted as a link. 2007 Arguments: 2008 storedText -- the source text to format 2009 """ 2010 if not storedText: 2011 return ('', '') 2012 linkMatch = linkRegExp.search(storedText) 2013 if not linkMatch: 2014 raise ValueError 2015 address, name = linkMatch.groups() 2016 return (address, name) 2017 2018 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 2019 """Return formatted output text from stored text for this field. 2020 2021 Arguments: 2022 storedText -- the source text to format 2023 oneLine -- if True, returns only first line of output (for titles) 2024 noHtml -- if True, removes all HTML markup (for titles, etc.) 2025 formatHtml -- if False, escapes HTML from prefix & suffix 2026 """ 2027 if noHtml: 2028 linkMatch = linkRegExp.search(storedText) 2029 if linkMatch: 2030 address, name = linkMatch.groups() 2031 storedText = name.strip() 2032 if not storedText: 2033 storedText = address.lstrip('#') 2034 return super().formatOutput(storedText, oneLine, noHtml, formatHtml) 2035 2036 def formatEditorText(self, storedText): 2037 """Return text formatted for use in the data editor. 2038 2039 Raises a ValueError if the data does not match the format. 2040 Arguments: 2041 storedText -- the source text to format 2042 """ 2043 if not storedText: 2044 return '' 2045 address, name = self.addressAndName(storedText) 2046 name = name.strip() 2047 if not name: 2048 name = urltools.shortName(address) 2049 return '{0} [{1}]'.format(address, name) 2050 2051 def storedText(self, editorText): 2052 """Return new text to be stored based on text from the data editor. 2053 2054 Raises a ValueError if the data does not match the format. 2055 Arguments: 2056 editorText -- the new text entered into the editor 2057 """ 2058 if not editorText: 2059 return '' 2060 nameMatch = linkSeparateNameRegExp.match(editorText) 2061 if nameMatch: 2062 address, name = nameMatch.groups() 2063 else: 2064 raise ValueError 2065 return '<a href="{0}">{1}</a>'.format(address.strip(), name.strip()) 2066 2067 def adjustedCompareValue(self, value): 2068 """Return value adjusted like the compareValue for use in conditionals. 2069 2070 Link fields use link address. 2071 Arguments: 2072 value -- the comparison value to adjust 2073 """ 2074 if not value: 2075 return '' 2076 try: 2077 address, name = self.addressAndName(value) 2078 except ValueError: 2079 return value.lower() 2080 return address.lstrip('#').lower() 2081 2082 2083class InternalLinkField(ExternalLinkField): 2084 """Class to handle a field containing internal links to nodes. 2085 2086 Stores data as HTML local link tag, shows in editors as "id [name]". 2087 """ 2088 typeName = 'InternalLink' 2089 editorClassName = 'IntLinkEditor' 2090 supportsInitDefault = False 2091 2092 def __init__(self, name, formatData=None): 2093 """Initialize a field format type. 2094 2095 Arguments: 2096 name -- the field name string 2097 formatData -- the attributes that define this field's format 2098 """ 2099 super().__init__(name, formatData) 2100 2101 def editorText(self, node): 2102 """Return text formatted for use in the data editor. 2103 2104 Raises a ValueError if the data does not match the format. 2105 Also raises a ValueError if the link is not a valid destination, with 2106 the editor text as the second argument to the exception. 2107 Arguments: 2108 node -- the tree item storing the data 2109 """ 2110 storedText = node.data.get(self.name, '') 2111 return self.formatEditorText(storedText, node.treeStructureRef()) 2112 2113 def formatEditorText(self, storedText, treeStructRef): 2114 """Return text formatted for use in the data editor. 2115 2116 Raises a ValueError if the data does not match the format. 2117 Also raises a ValueError if the link is not a valid destination, with 2118 the editor text as the second argument to the exception. 2119 Arguments: 2120 storedText -- the source text to format 2121 treeStructRef -- ref to the tree structure to get the linked title 2122 """ 2123 if not storedText: 2124 return '' 2125 address, name = self.addressAndName(storedText) 2126 address = address.lstrip('#') 2127 targetNode = treeStructRef.nodeDict.get(address, None) 2128 linkTitle = targetNode.title() if targetNode else _errorStr 2129 name = name.strip() 2130 if not name and targetNode: 2131 name = linkTitle 2132 result = 'LinkTo: {0} [{1}]'.format(linkTitle, name) 2133 if linkTitle == _errorStr: 2134 raise ValueError('invalid address', result) 2135 return result 2136 2137 def storedText(self, editorText): 2138 """Return new text to be stored based on text from the data editor. 2139 2140 Uses the "address [name]" format as input, not the final editor form. 2141 Raises a ValueError if the data does not match the format. 2142 Arguments: 2143 editorText -- the new editor text in "address [name]" format 2144 """ 2145 if not editorText: 2146 return '' 2147 nameMatch = linkSeparateNameRegExp.match(editorText) 2148 if not nameMatch: 2149 raise ValueError 2150 address, name = nameMatch.groups() 2151 if not address: 2152 raise ValueError('invalid address', '') 2153 if not name: 2154 name = _errorStr 2155 result = '<a href="#{0}">{1}</a>'.format(address.strip(), name.strip()) 2156 if name == _errorStr: 2157 raise ValueError('invalid name', result) 2158 return result 2159 2160 2161class PictureField(HtmlTextField): 2162 """Class to handle a field containing various types of external HTML links. 2163 2164 Protocol choices include http, https, file, mailto. 2165 Stores data as HTML tags, shows in editors as "protocol:address [name]". 2166 """ 2167 typeName = 'Picture' 2168 evalHtmlDefault = False 2169 editorClassName = 'PictureLinkEditor' 2170 sortTypeStr ='60_link' 2171 2172 def __init__(self, name, formatData=None): 2173 """Initialize a field format type. 2174 2175 Arguments: 2176 name -- the field name string 2177 formatData -- the attributes that define this field's format 2178 """ 2179 super().__init__(name, formatData) 2180 2181 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 2182 """Return formatted output text from stored text for this field. 2183 2184 Arguments: 2185 storedText -- the source text to format 2186 oneLine -- if True, returns only first line of output (for titles) 2187 noHtml -- if True, removes all HTML markup (for titles, etc.) 2188 formatHtml -- if False, escapes HTML from prefix & suffix 2189 """ 2190 if noHtml: 2191 linkMatch = _imageRegExp.search(storedText) 2192 if linkMatch: 2193 address = linkMatch.group(1) 2194 storedText = address.strip() 2195 return super().formatOutput(storedText, oneLine, noHtml, formatHtml) 2196 2197 def formatEditorText(self, storedText): 2198 """Return text formatted for use in the data editor. 2199 2200 Raises a ValueError if the data does not match the format. 2201 Arguments: 2202 storedText -- the source text to format 2203 """ 2204 if not storedText: 2205 return '' 2206 linkMatch = _imageRegExp.search(storedText) 2207 if not linkMatch: 2208 raise ValueError 2209 return linkMatch.group(1) 2210 2211 def storedText(self, editorText): 2212 """Return new text to be stored based on text from the data editor. 2213 2214 Raises a ValueError if the data does not match the format. 2215 Arguments: 2216 editorText -- the new text entered into the editor 2217 """ 2218 editorText = editorText.strip() 2219 if not editorText: 2220 return '' 2221 nameMatch = linkSeparateNameRegExp.match(editorText) 2222 if nameMatch: 2223 address, name = nameMatch.groups() 2224 else: 2225 address = editorText 2226 name = urltools.shortName(address) 2227 return '<img src="{0}" />'.format(editorText) 2228 2229 def adjustedCompareValue(self, value): 2230 """Return value adjusted like the compareValue for use in conditionals. 2231 2232 Link fields use link address. 2233 Arguments: 2234 value -- the comparison value to adjust 2235 """ 2236 if not value: 2237 return '' 2238 linkMatch = _imageRegExp.search(value) 2239 if not linkMatch: 2240 return value.lower() 2241 return linkMatch.group(1).lower() 2242 2243 2244class RegularExpressionField(HtmlTextField): 2245 """Class to handle a field format type controlled by a regular expression. 2246 2247 Stores options and format strings for a number field type. 2248 Provides methods to return formatted data. 2249 """ 2250 typeName = 'RegularExpression' 2251 defaultFormat = '.*' 2252 evalHtmlDefault = False 2253 fixEvalHtmlSetting = False 2254 editorClassName = 'LineEditor' 2255 formatHelpMenuList = [(_('Any Character\t.'), '.'), 2256 (_('End of Text\t$'), '$'), 2257 ('', ''), 2258 (_('0 Or More Repetitions\t*'), '*'), 2259 (_('1 Or More Repetitions\t+'), '+'), 2260 (_('0 Or 1 Repetitions\t?'), '?'), 2261 ('', ''), 2262 (_('Set of Numbers\t[0-9]'), '[0-9]'), 2263 (_('Lower Case Letters\t[a-z]'), '[a-z]'), 2264 (_('Upper Case Letters\t[A-Z]'), '[A-Z]'), 2265 (_('Not a Number\t[^0-9]'), '[^0-9]'), 2266 ('', ''), 2267 (_('Or\t|'), '|'), 2268 (_('Escape a Special Character\t\\'), '\\')] 2269 2270 def __init__(self, name, formatData=None): 2271 """Initialize a field format type. 2272 2273 Arguments: 2274 name -- the field name string 2275 formatData -- the dict that defines this field's format 2276 """ 2277 super().__init__(name, formatData) 2278 2279 def setFormat(self, format): 2280 """Set the format string and initialize as required. 2281 2282 Raise a ValueError if the format is illegal. 2283 Arguments: 2284 format -- the new format string 2285 """ 2286 try: 2287 re.compile(format) 2288 except re.error: 2289 raise ValueError 2290 super().setFormat(format) 2291 2292 def formatOutput(self, storedText, oneLine, noHtml, formatHtml): 2293 """Return formatted output text from stored text for this field. 2294 2295 Arguments: 2296 storedText -- the source text to format 2297 oneLine -- if True, returns only first line of output (for titles) 2298 noHtml -- if True, removes all HTML markup (for titles, etc.) 2299 formatHtml -- if False, escapes HTML from prefix & suffix 2300 """ 2301 match = re.fullmatch(self.format, saxutils.unescape(storedText)) 2302 if not storedText or match: 2303 text = storedText 2304 else: 2305 text = _errorStr 2306 return super().formatOutput(text, oneLine, noHtml, formatHtml) 2307 2308 def formatEditorText(self, storedText): 2309 """Return text formatted for use in the data editor. 2310 2311 Raises a ValueError if the data does not match the format. 2312 Arguments: 2313 storedText -- the source text to format 2314 """ 2315 if not self.evalHtml: 2316 storedText = saxutils.unescape(storedText) 2317 match = re.fullmatch(self.format, storedText) 2318 if not storedText or match: 2319 return storedText 2320 raise ValueError 2321 2322 def storedText(self, editorText): 2323 """Return new text to be stored based on text from the data editor. 2324 2325 Raises a ValueError if the data does not match the format. 2326 Arguments: 2327 editorText -- the new text entered into the editor 2328 """ 2329 match = re.fullmatch(self.format, editorText) 2330 if not editorText or match: 2331 if self.evalHtml: 2332 return editorText 2333 return saxutils.escape(editorText) 2334 raise ValueError 2335 2336 2337class AncestorLevelField(TextField): 2338 """Placeholder format for ref. to ancestor fields at specific levels. 2339 """ 2340 typeName = 'AncestorLevel' 2341 def __init__(self, name, ancestorLevel=1): 2342 """Initialize a field format placeholder type. 2343 2344 Arguments: 2345 name -- the field name string 2346 ancestorLevel -- the number of generations to go back 2347 """ 2348 super().__init__(name, {}) 2349 self.ancestorLevel = ancestorLevel 2350 2351 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 2352 """Return formatted output text for this field in this node. 2353 2354 Finds the appropriate ancestor node to get the field text. 2355 Arguments: 2356 node -- the tree node to start from 2357 oneLine -- if True, returns only first line of output (for titles) 2358 noHtml -- if True, removes all HTML markup (for titles, etc.) 2359 formatHtml -- if False, escapes HTML from prefix & suffix 2360 spotRef -- optional, used for ancestor field refs 2361 """ 2362 if not spotRef: 2363 spotRef = node.spotByNumber(0) 2364 for num in range(self.ancestorLevel): 2365 spotRef = spotRef.parentSpot 2366 if not spotRef: 2367 return '' 2368 try: 2369 field = spotRef.nodeRef.formatRef.fieldDict[self.name] 2370 except (AttributeError, KeyError): 2371 return '' 2372 return field.outputText(spotRef.nodeRef, oneLine, noHtml, formatHtml, 2373 spotRef) 2374 2375 def sepName(self): 2376 """Return the name enclosed with {* *} separators 2377 """ 2378 return '{{*{0}{1}*}}'.format(self.ancestorLevel * '*', self.name) 2379 2380 2381class AnyAncestorField(TextField): 2382 """Placeholder format for ref. to matching ancestor fields at any level. 2383 """ 2384 typeName = 'AnyAncestor' 2385 def __init__(self, name): 2386 """Initialize a field format placeholder type. 2387 2388 Arguments: 2389 name -- the field name string 2390 """ 2391 super().__init__(name, {}) 2392 2393 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 2394 """Return formatted output text for this field in this node. 2395 2396 Finds the appropriate ancestor node to get the field text. 2397 Arguments: 2398 node -- the tree node to start from 2399 oneLine -- if True, returns only first line of output (for titles) 2400 noHtml -- if True, removes all HTML markup (for titles, etc.) 2401 formatHtml -- if False, escapes HTML from prefix & suffix 2402 spotRef -- optional, used for ancestor field refs 2403 """ 2404 if not spotRef: 2405 spotRef = node.spotByNumber(0) 2406 while spotRef.parentSpot: 2407 spotRef = spotRef.parentSpot 2408 try: 2409 field = spotRef.nodeRef.formatRef.fieldDict[self.name] 2410 except (AttributeError, KeyError): 2411 pass 2412 else: 2413 return field.outputText(spotRef.nodeRef, oneLine, noHtml, 2414 formatHtml, spotRef) 2415 return '' 2416 2417 def sepName(self): 2418 """Return the name enclosed with {* *} separators 2419 """ 2420 return '{{*?{0}*}}'.format(self.name) 2421 2422 2423class ChildListField(TextField): 2424 """Placeholder format for ref. to matching ancestor fields at any level. 2425 """ 2426 typeName = 'ChildList' 2427 def __init__(self, name): 2428 """Initialize a field format placeholder type. 2429 2430 Arguments: 2431 name -- the field name string 2432 """ 2433 super().__init__(name, {}) 2434 2435 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 2436 """Return formatted output text for this field in this node. 2437 2438 Returns a joined list of matching child field data. 2439 Arguments: 2440 node -- the tree node to start from 2441 oneLine -- if True, returns only first line of output (for titles) 2442 noHtml -- if True, removes all HTML markup (for titles, etc.) 2443 formatHtml -- if False, escapes HTML from prefix & suffix 2444 spotRef -- optional, used for ancestor field refs 2445 """ 2446 result = [] 2447 for child in node.childList: 2448 try: 2449 field = child.formatRef.fieldDict[self.name] 2450 except KeyError: 2451 pass 2452 else: 2453 result.append(field.outputText(child, oneLine, noHtml, 2454 formatHtml, spotRef)) 2455 outputSep = node.formatRef.outputSeparator 2456 return outputSep.join(result) 2457 2458 def sepName(self): 2459 """Return the name enclosed with {* *} separators 2460 """ 2461 return '{{*&{0}*}}'.format(self.name) 2462 2463 2464class DescendantCountField(TextField): 2465 """Placeholder format for count of descendants at a given level. 2466 """ 2467 typeName = 'DescendantCount' 2468 def __init__(self, name, descendantLevel=1): 2469 """Initialize a field format placeholder type. 2470 2471 Arguments: 2472 name -- the field name string 2473 descendantLevel -- the level to descend to 2474 """ 2475 super().__init__(name, {}) 2476 self.descendantLevel = descendantLevel 2477 2478 def outputText(self, node, oneLine, noHtml, formatHtml, spotRef=None): 2479 """Return formatted output text for this field in this node. 2480 2481 Returns a count of descendants at the approriate level. 2482 Arguments: 2483 node -- the tree node to start from 2484 oneLine -- if True, returns only first line of output (for titles) 2485 noHtml -- if True, removes all HTML markup (for titles, etc.) 2486 formatHtml -- if False, escapes HTML from prefix & suffix 2487 spotRef -- optional, used for ancestor field refs 2488 """ 2489 newNodes = [node] 2490 for i in range(self.descendantLevel): 2491 prevNodes = newNodes 2492 newNodes = [] 2493 for child in prevNodes: 2494 newNodes.extend(child.childList) 2495 return repr(len(newNodes)) 2496 2497 def sepName(self): 2498 """Return the name enclosed with {* *} separators 2499 """ 2500 return '{{*#{0}*}}'.format(self.name) 2501 2502 2503#### Utility Functions #### 2504 2505def removeMarkup(text): 2506 """Return text with all HTML Markup removed and entities unescaped. 2507 2508 Any <br /> tags are replaced with newlines. 2509 """ 2510 text = _lineBreakRegEx.sub('\n', text) 2511 text = _stripTagRe.sub('', text) 2512 return saxutils.unescape(text) 2513 2514def adjOutDateFormat(dateFormat): 2515 """Replace Linux lead zero removal with Windows version in date formats. 2516 2517 Arguments: 2518 dateFormat -- the format to modify 2519 """ 2520 if sys.platform.startswith('win'): 2521 dateFormat = dateFormat.replace('%-', '%#') 2522 return dateFormat 2523 2524def adjInDateFormat(dateFormat): 2525 """Remove lead zero formatting in date formats for reading dates. 2526 2527 Arguments: 2528 dateFormat -- the format to modify 2529 """ 2530 return dateFormat.replace('%-', '%') 2531 2532def adjTimeAmPm(timeFormat, time): 2533 """Add AM/PM to timeFormat if in format and locale skips it. 2534 2535 Arguments: 2536 timeFormat -- the format to modify 2537 time -- the datetime object to check for AM/PM 2538 """ 2539 if '%p' in timeFormat and time.strftime('%I (%p)').endswith('()'): 2540 amPm = 'AM' if time.hour < 12 else 'PM' 2541 timeFormat = re.sub(r'(?<!%)%p', amPm, timeFormat) 2542 return timeFormat 2543 2544def translatedTypeName(typeName): 2545 """Return a translated type name. 2546 2547 Arguments: 2548 typeName -- the English type name 2549 """ 2550 try: 2551 return translatedFieldTypes[fieldTypes.index(typeName)] 2552 except ValueError: 2553 if typeName == 'DescendantCount': 2554 return _('DescendantCount') 2555 raise 2556