source: tracformsplugin/tags/tracforms-0.4.1/0.11/tracforms/macros.py

Last change on this file was 10490, checked in by Steffen Hoffmann, 12 years ago

TracFormsPlugin: Release maintenance version 0.4.1 for compatibility with Trac 0.11, closes #9000.

These are changes cherry-picked from trunk and merged into tracforms-0.4 to
establish full compatibility with Trac 0.11 on level with plugin's own claims.

File size: 21.3 KB
Line 
1# -*- coding: utf-8 -*-
2
3import StringIO
4import fnmatch
5import re
6import time
7import traceback
8
9from math import modf
10
11from trac.util.datefmt import format_datetime
12from trac.util.text import to_unicode
13from trac.wiki.macros import WikiMacroBase
14from trac.wiki.formatter import Formatter
15
16from api import FormDBUser, PasswordStoreUser, _
17from compat import json
18from environment import FormEnvironment
19from errors import FormError, FormTooManyValuesError
20from util import resource_from_page, xml_escape
21
22argRE = re.compile('\s*(".*?"|\'.*?\'|\S+)\s*')
23argstrRE = re.compile('%(.*?)%')
24tfRE = re.compile('\['
25    'tf(?:\.([a-zA-Z_]+?))?'
26    '(?::([^\[\]]*?))?'
27    '\]')
28kwtrans = {
29    'class'     : '_class',
30    'id'        : '_id',
31    }
32
33
34class TracFormMacro(WikiMacroBase, FormDBUser, PasswordStoreUser):
35    """Docs for TracForms macro..."""
36
37    def expand_macro(self, formatter, name, args):
38        processor = FormProcessor(self, formatter, name, args)
39        return processor.execute()
40
41
42class FormProcessor(object):
43    """Core parser and processor logic for TracForms markup."""
44
45    # Default state (beyond what is set in expand_macro).
46    showErrors = True
47    page = None
48    subcontext = None
49    default_op = 'checkbox'
50    allow_submit = False
51    keep_history = False
52    track_fields = True
53    submit_label = None
54    submit_name = None
55    form_class = None
56    form_cssid = None
57    form_name = None
58    sorted_env = None
59
60    def __init__(self, macro, formatter, name, args):
61        self.macro = macro
62        self.formatter = formatter
63        self.args = args
64        self.name = name
65
66    def execute(self):
67        formatter = self.formatter
68        args = self.args
69        name = self.name
70
71        # Look in the formatter req object for evidence we are executing.
72        self.subform = getattr(formatter.req, type(self).__name__, False)
73        if not self.subform:
74            setattr(formatter.req, type(self).__name__, True)
75        self.env = dict(getattr(formatter.req, 'tracform_env', ()))
76
77        # Setup preliminary context
78        self.page = formatter.req.path_info
79        if self.page == '/wiki' or self.page == '/wiki/':
80            self.page = '/wiki/WikiStart'
81        realm, resource_id = resource_from_page(formatter.env, self.page)
82
83        # Remove leading comments and process commands.
84        textlines = []
85        errors = []
86        srciter = iter(args.split('\n'))
87        for line in srciter:
88            if line[:1] == '#':
89                # Comment or operation.
90                line = line.strip()[1:]
91                if line[:1] == '!':
92                    # It's a command, parse the arguments...
93                    kw = {}
94                    args = list(self.getargs(line[1:], kw))
95                    if len(args):
96                        cmd = args.pop(0)
97                        fn = getattr(self, 'cmd_' + cmd.lower(), None)
98                        if fn is None:
99                            errors.append(
100                                _("ERROR: No TracForms command '%s'" % cmd))
101                        else:
102                            try:
103                                fn(*args, **kw)
104                            except FormError, e:
105                                errors.append(str(e))
106                            except Exception, e:
107                                errors.append(traceback.format_exc())
108            else:
109                if self.showErrors:
110                    textlines.extend(errors)
111                textlines.append(line)
112                textlines.extend(srciter)
113
114        # Determine our destination context and load the current state.
115        self.context = tuple([realm, resource_id,
116                              self.subcontext is not None and \
117                              self.subcontext or ''])
118        state = self.macro.get_tracform_state(self.context)
119        self.formatter.env.log.debug(
120            'TracForms state = ' + (state is not None and state or ''))
121        for name, value in json.loads(state or '{}').iteritems():
122            self.env[name] = value
123            self.formatter.env.log.debug(
124                name + ' = ' + to_unicode(value))
125            if self.subcontext is not None:
126                self.env[self.subcontext + ':' + name] = value
127        self.sorted_env = None
128        (self.form_id, self.form_realm, self.form_resource_id,
129            self.form_subcontext, self.form_updater, self.form_updated_on,
130            self.form_keep_history, self.form_track_fields) = \
131            self.macro.get_tracform_meta(self.context)
132        self.form_id = self.form_id is not None and int(self.form_id) or None
133
134        # Wiki-ize the text, this will allow other macros to execute after
135        # which we can do our own replacements within whatever formatted
136        # junk is left over.
137        text = self.wiki('\n'.join(textlines))
138
139        # Keep replacing tf: sections until there are no more
140        # replacements.  On each substitution, wiki() is called on the
141        # result.
142        self.updated = True
143        while self.updated:
144            self.updated = False
145            text = tfRE.sub(self.process, text)
146        setattr(formatter.req, type(self).__name__, None)
147
148        self.formatter.env.log.debug('TracForms parsing finished')
149        if 'FORM_EDIT_VAL' in formatter.perm:
150            self.allow_submit = True
151        return ''.join(self.build_form(text))
152
153    def build_form(self, text):
154        if not self.subform:
155            form_class = self.form_class
156            form_cssid = self.form_cssid or self.subcontext
157            form_name = self.form_name or self.subcontext
158            dest = self.formatter.req.href('/form/update')
159            yield ('<FORM class="printableform" ' +
160                    'method="POST" action=%r' % str(dest) +
161                    (form_cssid is not None 
162                        and ' id="%s"' % form_cssid
163                        or '') +
164                    (form_name is not None 
165                        and ' name="%s"' % form_name
166                        or '') +
167                    (form_class is not None 
168                        and ' class="%s"' % form_class
169                        or '') +
170                    '>')
171            yield text
172            if self.allow_submit:
173                # TRANSLATOR: Default submit button label
174                submit_label = self.submit_label or _("Update Form")
175                yield '<INPUT class="buttons" type="submit"'
176                if self.submit_name:
177                    yield ' name=%r' % str(self.submit_name)
178                yield ' value=%r' % xml_escape(submit_label)
179                yield '>'
180            if self.keep_history:
181                yield '<INPUT type="hidden"'
182                yield ' name="__keep_history__" value="yes">'
183            if self.track_fields:
184                yield '<INPUT type="hidden"'
185                yield ' name="__track_fields__" value="yes">'
186            if self.form_updated_on is not None:
187                yield '<INPUT type="hidden" name="__basever__"'
188                yield ' value="' + str(self.form_updated_on) + '">'
189            context = json.dumps(
190                self.context, separators=(',', ':'))
191            yield '<INPUT type="hidden" ' + \
192                'name="__context__" value=%r>' % context
193            backpath = self.formatter.req.href(self.formatter.req.path_info)
194            yield '<INPUT type="hidden" ' \
195                    'name="__backpath__" value=%s>' % str(backpath)
196            form_token = self.formatter.req.form_token
197            yield '<INPUT type="hidden" ' \
198                    'name="__FORM_TOKEN" value=%r>' % str(form_token)
199            yield '</FORM>'
200        else:
201            yield text
202
203    def getargs(self, argstr, kw=None):
204        if kw is None:
205            kw = {}
206        for arg in argRE.findall(argstrRE.sub(self.argsub, argstr) or ''):
207            if arg[:1] in '"\'':
208                arg = arg[1:-1]
209            if arg[:1] == '-':
210                try:
211                    arg = (str(float(arg)))
212                    yield arg
213                except ValueError, e:
214                    name, value = (arg[1:].split('=', 1) + [True])[:2]
215                    kw[str(kwtrans.get(name, name))] = value
216                    pass
217            else:
218                yield arg
219
220    def argsub(self, match, NOT_FOUND=KeyError, aslist=False):
221        if isinstance(match, basestring):
222            name = match
223        else:
224            name = match.group(1)
225        if name[:1] in '"\'':
226            quote = True
227            name = name[1:-1]
228        else:
229            quote = False
230        if '*' in name or '?' in name or '[' in name:
231            value = []
232            keys = self.get_sorted_env()
233            for key in fnmatch.filter(keys, name):
234                obj = self.env[key]
235                if isinstance(obj, (tuple, list)):
236                    value.extend(obj)
237                else:
238                    value.append(obj)
239            if not value and self.page:
240                for key in fnmatch.filter(keys, self.page + ':' + name):
241                    obj = self.env[key]
242                    if isinstance(obj, (tuple, list)):
243                        value.extend(obj)
244                    else:
245                        value.append(obj)
246            if not value and self.subcontext:
247                for key in fnmatch.filter(keys, self.subcontext + ':' + name):
248                    obj = self.env[key]
249                    if isinstance(obj, (tuple, list)):
250                        value.extend(obj)
251                    else:
252                        value.append(obj)
253            if not value:
254                for key in fnmatch.filter(keys, name):
255                    obj = self.env[key]
256                    if isinstance(obj, (tuple, list)):
257                        value.extend(obj)
258                    else:
259                        value.append(obj)
260        else:
261            value = self.env.get(name, NOT_FOUND)
262            if self.page is not None and value is NOT_FOUND:
263                value = self.env.get(self.page + ':' + name, NOT_FOUND)
264            if self.subcontext is not None and value is NOT_FOUND:
265                value = self.env.get(self.subcontext + ':' + name, NOT_FOUND)
266            if value is NOT_FOUND:
267                value = self.env.get(name, NOT_FOUND)
268            if value is NOT_FOUND:
269                fn = getattr(self, 'env:' + name.lower(), None)
270                if fn is not None:
271                    value = fn()
272                else:
273                    value = ''
274        if aslist:
275            if isinstance(value, (list, tuple)):
276                return tuple(value)
277            else:
278                return (value,)
279        else:
280            if isinstance(value, (list, tuple)):
281                return ' '.join(
282                    quote and repr(str(item)) or str(item) for item in value)
283            else:
284                value = str(value)
285                if quote:
286                    value = repr(value)
287                return value
288
289    def get_sorted_env(self):
290        if self.sorted_env is None:
291            self.sorted_env = sorted(self.env)
292        return self.sorted_env
293
294    def env_user(self):
295        return self.req.authname
296
297    def env_now(self):
298        return time.strftime(time.localtime(time.time()))
299
300    def cmd_errors(self, show):
301        self.showErrors = show.upper() in ('SHOW', 'TRUE', 'YES')
302
303    def cmd_page(self, page):
304        if page.upper() in ('NONE', 'DEFAULT', 'CURRENT'):
305            self.page = None
306        else:
307            self.page = page
308
309    def cmd_subcontext(self, context):
310        if context.lower() == 'none':
311            self.subcontext = None
312        else:
313            self.subcontext = str(context)
314
315    def cmd_load(self, subcontext, page=None):
316        if page is None:
317            page = self.page
318        context = page + ':' + subcontext
319        state = self.macro.get_tracform_state(context)
320        for name, value in json.loads(state or '{}').iteritems():
321            self.env[context + ':' + name] = value
322            if self.subcontext is not None:
323                self.env[self.subcontext + ':' + name] = value
324
325    def cmd_class(self, value):
326        self.form_class = value
327
328    def cmd_id(self, value):
329        self.form_cssid = value
330
331    def cmd_name(self, value):
332        self.form_name = value
333
334    def cmd_default(self, default):
335        self.default_op = default
336
337    def cmd_track_fields(self, track='yes'):
338        self.track_fields = track.lower() == 'yes'
339
340    def cmd_keep_history(self, track='yes'):
341        self.keep_history = track.lower() == 'yes'
342
343    def cmd_submit_label(self, label):
344        self.submit_label = label
345
346    def cmd_submit_name(self, name):
347        self.submit_name
348
349    def cmd_setenv(self, name, value):
350        self.env[name] = value
351        self.sorted_env = None
352
353    def cmd_setlist(self, name, *values):
354        self.env[name] = tuple(values)
355        self.sorted_env = None
356
357    def cmd_operation(_self, _name, _op, *_args, **_kw):
358        if _op in ('is', 'as'):
359            _op, _args = _args[0], _args[1:]
360        op = getattr(_self, 'op_' + _op, None)
361        if op is None:
362            raise FormTooManyValuesError(str(_name))
363        def partial(*_newargs, **_newkw):
364            if _kw or _newkw:
365                kw = dict(_kw)
366                kw.update(_newkw)
367            else:
368                kw = {}
369            return op(*(_newargs + _args), **kw)
370        _self.env['op:' + _name] = partial
371
372    def wiki(self, text):
373        out = StringIO.StringIO()
374        Formatter(self.formatter.env, self.formatter.context).format(text, out)
375        return out.getvalue()
376
377    def process(self, m):
378        self.updated = True
379        op, argstr = m.groups()
380        op = op or self.default_op
381        self.formatter.env.log.debug('Converting TracForms op: ' + str(op))
382        kw = {}
383        args = tuple(self.getargs(argstr, kw))
384        fn = self.env.get('op:' + op.lower())
385        if fn is None:
386            fn = getattr(self, 'op_' + op.lower(), None)
387        if fn is None:
388            raise FormTooManyValuesError(str(op))
389        else:
390            try:
391                if op[:5] == 'wikiop_':
392                    self.formatter.env.log.debug(
393                        'TracForms wiki value: ' + self.wiki(str(fn(*args))))
394                    return self.wiki(str(fn(*args)))
395                else:
396                    self.formatter.env.log.debug(
397                        'TracForms value: ' + to_unicode(fn(*args, **kw)))
398                    return to_unicode(fn(*args, **kw))
399            except FormError, e:
400                return '<PRE>' + str(e) + '</PRE>'
401            except Exception, e:
402                return '<PRE>' + traceback.format_exc() + '</PRE>'
403
404    def op_test(self, *args):
405        return repr(args)
406
407    def wikiop_value(self, field):
408        return 'VALUE=' + field
409
410    def get_field(self, name, default=None, make_single=True):
411        current = self.env.get(name, default)
412        if make_single and isinstance(current, (tuple, list)):
413            if len(current) == 0:
414                current = default
415            elif len(current) == 1:
416                current = current[0]
417            else:
418                raise FormTooManyValuesError(str(name))
419        return current
420
421    def op_input(self, field, content=None, size=None, _id=None, _class=None):
422        current = self.get_field(field)
423        if current is not None:
424            content = current
425        return ("<INPUT name='%s'" % field +
426                (size is not None and ' size="%s"' % size or '') +
427                (_id is not None and ' id="%s"' % _id or '') +
428                (_class is not None and ' class="%s"' % _class or '') +
429                (content is not None and (" value=%r" % xml_escape(
430                                                     content)) or '') +
431                '>')
432
433    def op_checkbox(self, field, value=None, _id=None, _class=None):
434        current = self.get_field(field)
435        if value is not None:
436            checked = value == current
437        else:
438            checked = bool(current)
439        return ("<INPUT type='checkbox' name='%s'" % field +
440                (_id is not None and ' id="%s"' % _id or '') +
441                (_class is not None and ' class="%s"' % _class or '') +
442                (value and (' value="' + value + '"') or '') +
443                (checked and ' checked' or '') +
444                '>')
445
446    def op_radio(self, field, value, _id=None, _class=None):
447        current = self.get_field(field)
448        return ("<INPUT type='radio' name='%s'" % field +
449                (_id is not None and ' id="%s"' % _id or '') +
450                (_class is not None and ' class="%s"' % _class or '') +
451                " value='%s'" % value +
452                (current == value and ' checked' or '') +
453                '>')
454
455    def op_select(self, field, *values, **kw):
456        _id = kw.pop('_id', None)
457        _class = kw.pop('_class', None)
458        current = self.get_field(field)
459        result = []
460        result.append("<SELECT name='%s'" % field +
461                (_id is not None and ' id="%s"' % _id or '') +
462                (_class is not None and ' class="%s"' % _class or '') +
463                '>')
464        for value in values:
465            value, label = (value.split('//', 1) + [value])[:2]
466            result += ("<OPTION value='%s'" % value.strip() +
467                    (current == value and ' selected' or '') +
468                    '>' + label.strip() + '</OPTION>')
469        result.append("</SELECT>")
470        return ''.join(result)
471
472    def op_textarea(self, field, content='',
473                    cols=None, rows=None,
474                    _id=None, _class=None):
475        current = self.get_field(field)
476        if current is not None:
477            content = current
478        return ("<TEXTAREA name='%s'" % field +
479                (cols is not None and ' cols="%s"' % cols or '') +
480                (rows is not None and ' rows="%s"' % rows or '') +
481                (_id is not None and ' id="%s"' % _id or '') +
482                (_class is not None and ' class="%s"' % _class or '') +
483                '>' + content + '</TEXTAREA>')
484
485    def op_context(self):
486        return str(self.context)
487
488    def op_who(self, field):
489        # TRANSLATOR: Default updater name
490        who = self.macro.get_tracform_fieldinfo(
491                self.context, field)[0] or _("unknown")
492        return who
493       
494    def op_when(self, field, format='%m/%d/%Y %H:%M:%S'):
495        when = self.macro.get_tracform_fieldinfo(self.context, field)[1]
496        return (when is not None and format_datetime(
497                when, format=str(format)) or _("unknown"))
498
499    def op_id(self):
500        return id(self)
501
502    def op_subform(self):
503        return self.subform
504
505    def op_form_id(self):
506        return self.form_id
507
508    def op_form_context(self):
509        return self.form_context
510
511    def op_form_updater(self):
512        return self.form_updater
513
514    def op_form_updated_on(self, format='%m/%d/%Y %H:%M:%S'):
515        return time.strftime(format, time.localtime(self.form_updated_on))
516
517    def op_sum(self, *values):
518        """Full precision summation using multiple floats for intermediate
519           values
520        """
521        ## msum() from http://code.activestate.com/recipes/393090/ (r5)
522        # Depends on IEEE-754 arithmetic guarantees.
523        partials = []               # sorted, non-overlapping partial sums
524        for x in values:
525            x = float(x)
526            i = 0
527            for y in partials:
528                if abs(x) < abs(y):
529                    x, y = y, x
530                hi = x + y
531                lo = y - (hi - x)
532                if lo:
533                    partials[i] = lo
534                    i += 1
535                x = hi
536            partials[i:] = [x]
537        result = sum(partials, 0.0)
538        # finally reduce display precision to integer, if possible
539        if modf(result)[0] == 0:
540            return str(int(result))
541        return str(result)
542
543    def op_sumif(self, check, *values):
544        return self.op_sum(*self.filter(check, values))
545
546    def op_count(self, *values):
547        return str(len(values))
548
549    def op_countif(self, check, *values):
550        return self.op_count(*self.filter(check, values))
551
552    def op_filter(self, check, *values):
553        return ' '.join(str(item) for item in self.filter(check, values))
554
555    def filter(self, check, values):
556        if check[:1] == '/' and check[-1:] == '/':
557            return re.findall(check[1: -1], values)
558        elif '*' in check or '?' in check or '[' in check:
559            return fnmatch.filter(values, check)
560        else:
561            return [i for i in values if check == i]
562
563    def op_sumprod(self, *values, **kw):
564        stride = int(kw.pop('stride', 2))
565        total = 0
566        irange = range(stride)
567        for index in xrange(0, len(values), stride):
568            row = 1.0
569            for inner in irange:
570                row *= float(values[inner + index])
571            total += row
572        return str(total)
573
574    def op_int(self, *values):
575        return ' '.join(str(int(float(value))) for value in values)
576
577    def op_value(self, *names):
578        return ' '.join(self.argsub(name) for name in names)
579
580    def op_quote(self, *names):
581        return ' '.join(repr(self.argsub(name)) for name in names)
582
583    def op_zip(self, *names):
584        zipped = zip(*(self.argsub(name, aslist=True) for name in names))
585        return ' '.join(' '.join(str(item) for item in level)
586                        for level in zipped)
587
588    def op_env_debug(self, pattern='*'):
589        result = []
590        for key in fnmatch.filter(self.get_sorted_env(), pattern):
591            result.append('%s = %s<BR>' % (key, self.env[key]))
592        return ''.join(result)
593
Note: See TracBrowser for help on using the repository browser.