| 1 | # -*- coding: utf-8 -*- |
|---|
| 2 | # |
|---|
| 3 | # Copyright (C) 2011-2012 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 json |
|---|
| 11 | |
|---|
| 12 | from trac.config import Option |
|---|
| 13 | from trac.core import Component, implements |
|---|
| 14 | from trac.notification.mail import EmailDistributor |
|---|
| 15 | from trac.perm import IPermissionRequestor, PermissionCache |
|---|
| 16 | from trac.util.html import html |
|---|
| 17 | from trac.util.translation import _ |
|---|
| 18 | from trac.web.chrome import ITemplateProvider, add_ctxtnav, add_script, \ |
|---|
| 19 | add_script_data, add_stylesheet |
|---|
| 20 | from trac.web.main import IRequestFilter, IRequestHandler |
|---|
| 21 | |
|---|
| 22 | MODE = 'quietmode' |
|---|
| 23 | LISTEN = 'quietlisten' |
|---|
| 24 | |
|---|
| 25 | |
|---|
| 26 | class QuietEmailDistributor(EmailDistributor): |
|---|
| 27 | """Specializes Announcer's email distributor to honor quiet mode.""" |
|---|
| 28 | def distribute(self, transport, recipients, event): |
|---|
| 29 | if hasattr(event, 'author') and \ |
|---|
| 30 | self._is_quiet_mode(event.author) and \ |
|---|
| 31 | 'QUIET_MODE' in PermissionCache(self.env, event.author): |
|---|
| 32 | self.log.debug("%s skipping distribution of %s because quiet " |
|---|
| 33 | "mode is enabled for %s", self.__class__.__name__, |
|---|
| 34 | event.__class__.__name__, event.author) |
|---|
| 35 | return |
|---|
| 36 | self.log.debug("%s dispatching to EmailDistributor", |
|---|
| 37 | self.__class__.__name__) |
|---|
| 38 | super(QuietEmailDistributor, self).distribute(transport, recipients, |
|---|
| 39 | event) |
|---|
| 40 | |
|---|
| 41 | def _is_quiet_mode(self, user): |
|---|
| 42 | for val, in self.env.db_query(""" |
|---|
| 43 | SELECT value FROM session_attribute |
|---|
| 44 | WHERE sid=%s AND authenticated=1 AND name=%s |
|---|
| 45 | """, (user, MODE)): |
|---|
| 46 | return val == '1' |
|---|
| 47 | else: |
|---|
| 48 | return False |
|---|
| 49 | |
|---|
| 50 | |
|---|
| 51 | class QuietBase(object): |
|---|
| 52 | """Shared class for common methods.""" |
|---|
| 53 | |
|---|
| 54 | enter_label = Option('quiet', 'enter_label', _('Enter Quiet Mode')) |
|---|
| 55 | leave_label = Option('quiet', 'leave_label', _('Leave Quiet Mode')) |
|---|
| 56 | |
|---|
| 57 | def _get_label(self, req, is_quiet=None): |
|---|
| 58 | if is_quiet is None: |
|---|
| 59 | is_quiet = self._is_quiet(req) |
|---|
| 60 | return is_quiet and _(self.leave_label) or _(self.enter_label) |
|---|
| 61 | |
|---|
| 62 | def _set_quiet_action(self, req, action): |
|---|
| 63 | if action == 'toggle': |
|---|
| 64 | return self._set_quiet(req, not self._is_quiet(req)) |
|---|
| 65 | elif action in ('enter', 'leave'): |
|---|
| 66 | return self._set_quiet(req, action == 'enter') |
|---|
| 67 | else: |
|---|
| 68 | return self._is_quiet(req) |
|---|
| 69 | |
|---|
| 70 | def _is_quiet(self, req): |
|---|
| 71 | """Returns true if the user requested quiet mode.""" |
|---|
| 72 | val = req.session.get(MODE, '0') |
|---|
| 73 | return val == '1' |
|---|
| 74 | |
|---|
| 75 | def _set_quiet(self, req, yes): |
|---|
| 76 | """Set or unset quiet mode for the user.""" |
|---|
| 77 | val = yes and '1' or '0' |
|---|
| 78 | req.session[MODE] = val |
|---|
| 79 | req.session.save() |
|---|
| 80 | return val == '1' |
|---|
| 81 | |
|---|
| 82 | |
|---|
| 83 | class QuietModule(Component, QuietBase): |
|---|
| 84 | implements(IRequestFilter, ITemplateProvider, IPermissionRequestor) |
|---|
| 85 | |
|---|
| 86 | # IPermissionRequestor methods |
|---|
| 87 | def get_permission_actions(self): |
|---|
| 88 | return ['QUIET_MODE'] |
|---|
| 89 | |
|---|
| 90 | # ITemplateProvider methods |
|---|
| 91 | def get_htdocs_dirs(self): |
|---|
| 92 | from pkg_resources import resource_filename |
|---|
| 93 | return [('quiet', resource_filename(__name__, 'htdocs'))] |
|---|
| 94 | |
|---|
| 95 | def get_templates_dirs(self): |
|---|
| 96 | return [] |
|---|
| 97 | |
|---|
| 98 | # IRequestFilter methods |
|---|
| 99 | def pre_process_request(self, req, handler): |
|---|
| 100 | return handler |
|---|
| 101 | |
|---|
| 102 | def post_process_request(self, req, template, data, content_type): |
|---|
| 103 | if 'QUIET_MODE' in req.perm and \ |
|---|
| 104 | req.path_info.startswith(('/ticket', '/newticket', |
|---|
| 105 | '/changeset', '/query', '/report')): |
|---|
| 106 | href = req.href(MODE, 'toggle') |
|---|
| 107 | a = html.a(self._get_label(req), href=href, id=MODE) |
|---|
| 108 | add_ctxtnav(req, a) |
|---|
| 109 | add_script(req, 'quiet/quiet.js') |
|---|
| 110 | add_stylesheet(req, 'quiet/quiet.css') |
|---|
| 111 | add_script_data(req, {'quiet': {'toggle': MODE, |
|---|
| 112 | 'listen': LISTEN}}) |
|---|
| 113 | return template, data, content_type |
|---|
| 114 | |
|---|
| 115 | |
|---|
| 116 | class QuietAjaxModule(Component, QuietBase): |
|---|
| 117 | implements(IRequestHandler) |
|---|
| 118 | |
|---|
| 119 | # IRequestHandler methods |
|---|
| 120 | def match_request(self, req): |
|---|
| 121 | return req.path_info.startswith('/' + MODE) |
|---|
| 122 | |
|---|
| 123 | def process_request(self, req): |
|---|
| 124 | try: |
|---|
| 125 | action = req.path_info[req.path_info.rfind('/') + 1:] |
|---|
| 126 | is_quiet = self._set_quiet_action(req, action) |
|---|
| 127 | data = {'label': self._get_label(req, is_quiet), |
|---|
| 128 | 'is_quiet': is_quiet} |
|---|
| 129 | process_json(req, data) |
|---|
| 130 | except Exception: |
|---|
| 131 | process_error(req) |
|---|
| 132 | |
|---|
| 133 | |
|---|
| 134 | class QuietListenerAjaxModule(Component): |
|---|
| 135 | implements(IRequestHandler) |
|---|
| 136 | |
|---|
| 137 | # IRequestHandler methods |
|---|
| 138 | def match_request(self, req): |
|---|
| 139 | return req.path_info.startswith('/' + LISTEN) |
|---|
| 140 | |
|---|
| 141 | def process_request(self, req): |
|---|
| 142 | try: |
|---|
| 143 | data = self._get_listeners(req) |
|---|
| 144 | process_json(req, data) |
|---|
| 145 | except Exception: |
|---|
| 146 | process_error(req) |
|---|
| 147 | |
|---|
| 148 | def _get_listeners(self, req): |
|---|
| 149 | listeners = [] |
|---|
| 150 | for key, action in self.env.config.options('quiet'): |
|---|
| 151 | if not key.endswith('.action'): |
|---|
| 152 | continue |
|---|
| 153 | num = key.split('.', 1)[0] |
|---|
| 154 | only, eq = self.env.config.get('quiet', num + '.only_if', ''), '' |
|---|
| 155 | if only and '=' in only: |
|---|
| 156 | only, eq = only.split('=', 1) |
|---|
| 157 | submit = self.env.config.get('quiet', num+'.submit', |
|---|
| 158 | 'false').lower() |
|---|
| 159 | listeners.append({ |
|---|
| 160 | 'action': action, |
|---|
| 161 | 'selector': self.env.config.get('quiet', |
|---|
| 162 | num + '.selector', ''), |
|---|
| 163 | 'only': only, 'eq': eq, |
|---|
| 164 | 'submit': submit == 'true', |
|---|
| 165 | }) |
|---|
| 166 | return listeners |
|---|
| 167 | |
|---|
| 168 | |
|---|
| 169 | def process_json(req, data): |
|---|
| 170 | try: |
|---|
| 171 | process_msg(req, 200, 'application/json', json.dumps(data)) |
|---|
| 172 | except Exception: |
|---|
| 173 | process_error(req) |
|---|
| 174 | |
|---|
| 175 | |
|---|
| 176 | def process_error(req): |
|---|
| 177 | import traceback |
|---|
| 178 | msg = "Oops...\n" + traceback.format_exc() + "\n" |
|---|
| 179 | process_msg(req, 500, 'text/plain', msg) |
|---|
| 180 | |
|---|
| 181 | |
|---|
| 182 | def process_msg(req, code, type, msg): |
|---|
| 183 | req.send_response(code) |
|---|
| 184 | req.send_header('Content-Type', type) |
|---|
| 185 | req.send_header('Content-Length', len(msg)) |
|---|
| 186 | req.end_headers() |
|---|
| 187 | req.write(msg) |
|---|