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