1 | # -*- coding: utf-8 -*- |
---|
2 | # |
---|
3 | # Copyright (C) 2010-2014 Rob Guttman <guttman@alum.mit.edu> |
---|
4 | # All rights reserved. |
---|
5 | # |
---|
6 | # This software is licensed as described in the file COPYING, which |
---|
7 | # you should have received as part of this distribution. |
---|
8 | # |
---|
9 | |
---|
10 | import re |
---|
11 | from pkg_resources import resource_filename |
---|
12 | |
---|
13 | from trac.core import * |
---|
14 | from trac.web.api import IRequestFilter, IRequestHandler |
---|
15 | from trac.web.chrome import ITemplateProvider, add_script, add_stylesheet |
---|
16 | from trac.config import ListOption |
---|
17 | from trac.prefs.api import IPreferencePanelProvider |
---|
18 | from trac.util.translation import domain_functions |
---|
19 | |
---|
20 | from dynfields.options import Options |
---|
21 | from dynfields.rules import add_domain, _, IRule |
---|
22 | |
---|
23 | |
---|
24 | class DynamicFieldsModule(Component): |
---|
25 | """A module that dynamically alters ticket fields based an extensible |
---|
26 | set of rules. Uses jQuery for full implementation.""" |
---|
27 | |
---|
28 | implements(IRequestFilter, IRequestHandler, ITemplateProvider, |
---|
29 | IPreferencePanelProvider) |
---|
30 | |
---|
31 | rules = ExtensionPoint(IRule) |
---|
32 | |
---|
33 | def __init__(self): |
---|
34 | # bind the 'dynfields' catalog to the specified locale directory |
---|
35 | locale_dir = resource_filename(__name__, 'locale') |
---|
36 | add_domain(self.env.path, locale_dir) |
---|
37 | |
---|
38 | # ITemplateProvider methods |
---|
39 | def get_htdocs_dirs(self): |
---|
40 | return [('dynfields', resource_filename(__name__, 'htdocs'))] |
---|
41 | |
---|
42 | def get_templates_dirs(self): |
---|
43 | return [resource_filename(__name__, 'templates')] |
---|
44 | |
---|
45 | # IRequestFilter methods |
---|
46 | def pre_process_request(self, req, handler): |
---|
47 | return handler |
---|
48 | |
---|
49 | def post_process_request(self, req, template, data, content_type): |
---|
50 | if ((req.path_info.startswith('/ticket') or |
---|
51 | req.path_info.startswith('/newticket')) and \ |
---|
52 | req.perm.has_permission('TICKET_MODIFY') \ |
---|
53 | ) or ( \ |
---|
54 | req.path_info.startswith('/query') and \ |
---|
55 | req.perm.has_permission('REPORT_VIEW')): |
---|
56 | # don't remove leading '/' character |
---|
57 | add_script(req, '/dynfields/dynfields.js') |
---|
58 | add_script(req, 'dynfields/layout.js') |
---|
59 | add_script(req, 'dynfields/rules.js') |
---|
60 | return template, data, content_type |
---|
61 | |
---|
62 | # IRequestHandler methods |
---|
63 | def match_request(self, req): |
---|
64 | return req.path_info.startswith('/dynfields') |
---|
65 | |
---|
66 | def process_request(self, req): |
---|
67 | data = {'triggers':self._get_triggers(req)} |
---|
68 | return 'dynfields.html', {'data': data},'text/javascript' |
---|
69 | |
---|
70 | def _get_triggers(self, req): |
---|
71 | """Converts trac.ini config to dict of triggers with rule specs.""" |
---|
72 | triggers = {} |
---|
73 | opts = Options(self.env) |
---|
74 | for key in opts: |
---|
75 | # extract the target field |
---|
76 | target_re = re.compile(r"(?P<target>[^.]+).*") |
---|
77 | target = target_re.match(key).groupdict()['target'] |
---|
78 | |
---|
79 | # extract rule specifications from configs |
---|
80 | for rule in self.rules: |
---|
81 | trigger = rule.get_trigger(target, key, opts) |
---|
82 | if not trigger: |
---|
83 | continue |
---|
84 | if not opts.is_enabled(req, key): |
---|
85 | continue |
---|
86 | value,_ = opts.get_value_and_options(req, target, key) |
---|
87 | spec = {'rule_name':rule.name, 'trigger':trigger, |
---|
88 | 'target':target, 'value':value} |
---|
89 | rule.update_spec(req, key, opts, spec) |
---|
90 | if trigger not in triggers: |
---|
91 | triggers[trigger] = [] # rule specs |
---|
92 | triggers[trigger].append(spec) |
---|
93 | |
---|
94 | return triggers |
---|
95 | |
---|
96 | # IPreferencePanelProvider methods |
---|
97 | def get_preference_panels(self, req): |
---|
98 | if self._get_prefs_data(req): # only show if there are preferences |
---|
99 | # TRANSLATOR: the preferences tab label |
---|
100 | yield 'dynfields', _("Dynamic Fields") |
---|
101 | |
---|
102 | def render_preference_panel(self, req, panel): |
---|
103 | opts = Options(self.env) |
---|
104 | if req.method == 'POST': |
---|
105 | opts.set_prefs(req) |
---|
106 | data = self._get_prefs_data(req, opts) |
---|
107 | return 'prefs_panel.html', {'data':data, 'saved':req.method == 'POST'} |
---|
108 | |
---|
109 | def _get_prefs_data(self, req, opts=None): |
---|
110 | """Returns the pref data, a dict of rule class titles whose values |
---|
111 | include lists of rule spec preference dicts each with these keys: |
---|
112 | |
---|
113 | id (based on unique key) |
---|
114 | label (of checkbox) |
---|
115 | enabled ('1' or '0') |
---|
116 | type ('none' or 'select'; TODO: 'text') |
---|
117 | options (list of options if type is 'select') |
---|
118 | value (saved preference or default value) |
---|
119 | """ |
---|
120 | if opts is None: |
---|
121 | opts = Options(self.env) |
---|
122 | data = {} |
---|
123 | for rule in self.rules: |
---|
124 | for key in opts: |
---|
125 | if not opts.has_pref(key): |
---|
126 | continue |
---|
127 | target_re = re.compile(r"(?P<target>[^.]+).*") |
---|
128 | target = target_re.match(key).groupdict()['target'] |
---|
129 | trigger = rule.get_trigger(target, key, opts) |
---|
130 | if not trigger: |
---|
131 | continue |
---|
132 | |
---|
133 | # this rule spec has a pref - so get it! |
---|
134 | pref = opts.get_pref(req, target, key) |
---|
135 | rule.update_pref(req, trigger, target, key, opts, pref) |
---|
136 | if rule.title not in data: |
---|
137 | data[rule.title] = {'desc':rule.desc, 'prefs':[]} |
---|
138 | data[rule.title]['prefs'].append(pref) |
---|
139 | return data |
---|