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