1 | from genshi.builder import tag |
---|
2 | from genshi.filters import Transformer |
---|
3 | |
---|
4 | from trac.core import implements, Component |
---|
5 | from trac.ticket.api import ITicketActionController |
---|
6 | from trac.ticket.default_workflow import ConfigurableTicketWorkflow |
---|
7 | from trac.util import get_reporter_id |
---|
8 | from trac.web.chrome import add_warning, Chrome |
---|
9 | from trac.web.api import ITemplateStreamFilter |
---|
10 | |
---|
11 | class CCHandlerActionController(Component): |
---|
12 | """ Automatically CC the User based on his/her selection of a field on the screen |
---|
13 | |
---|
14 | Don't forget to add `CCHandlerActionController` to the workflow |
---|
15 | option in [ticket]. |
---|
16 | If there is no workflow option, the line will look like this: |
---|
17 | |
---|
18 | workflow = ConfigurableTicketWorkflow, CCHandlerActionController |
---|
19 | """ |
---|
20 | implements(ITemplateStreamFilter, ITicketActionController) |
---|
21 | |
---|
22 | def __init__(self): |
---|
23 | self.workflow = ConfigurableTicketWorkflow(self.env) |
---|
24 | |
---|
25 | # ITemplateStreamFilter methods |
---|
26 | |
---|
27 | def filter_stream(self, req, method, filename, stream, data): |
---|
28 | if filename == 'ticket.html' and req.authname != 'anonymous': |
---|
29 | ticket = data.get('ticket') |
---|
30 | if ticket and ticket.exists: |
---|
31 | #filter = Transformer("div[@class='field']/fieldset[@class='iefix']") |
---|
32 | #filter = Transformer("div[@id='content' and @class='ticket']/div[@id='ticket']") |
---|
33 | filter = Transformer("//form[@id='propertyform']/div[@class='field']") |
---|
34 | #Hide the CC checkbox if it is present |
---|
35 | if data.has_key('fields'): |
---|
36 | for dict in data['fields']: |
---|
37 | if dict['name'] == 'cc': |
---|
38 | if dict.has_key('cc_entry'): |
---|
39 | dict['skip'] = True |
---|
40 | dict['rendered'] = 'no' |
---|
41 | break |
---|
42 | |
---|
43 | |
---|
44 | return stream | filter.before(self._append_CC_radio(req, ticket)) |
---|
45 | return stream |
---|
46 | |
---|
47 | # ITicketActionController methods |
---|
48 | |
---|
49 | def get_ticket_actions(self, req, ticket): |
---|
50 | #We need to handle all statuses so that we can perform the action regardless of which action was selected |
---|
51 | #TODO: What about other actions not in ConfigurableTicketWorkflow? (We'll worry about it when it happens) |
---|
52 | return self.workflow.get_ticket_actions(req, ticket) |
---|
53 | |
---|
54 | def get_all_status(self): |
---|
55 | return [] |
---|
56 | |
---|
57 | def render_ticket_action_control(self, req, ticket, action): |
---|
58 | return (None, None, None) |
---|
59 | |
---|
60 | def get_ticket_changes(self, req, ticket, action): |
---|
61 | return {} |
---|
62 | |
---|
63 | def apply_action_side_effects(self, req, ticket, action): |
---|
64 | if (action != 'delete' and 'cc_update_manual' in req.args): |
---|
65 | cc_action, cc_entry, cc_list = self._tweak_cc(req, ticket['cc'], req.args['cc_update_manual'] == 'yes') |
---|
66 | |
---|
67 | if cc_action == 'remove': |
---|
68 | self.log.info("Removing CC %s on ticket %s" % (cc_entry, ticket.id)) |
---|
69 | #add_warning(req, "You have been removed from the Cc list.") |
---|
70 | cc_list.remove(cc_entry) |
---|
71 | elif cc_action == 'add': |
---|
72 | self.log.info("Adding CC %s on ticket %s" % (cc_entry, ticket.id)) |
---|
73 | #add_warning(req, "You have been added to the Cc list.") |
---|
74 | cc_list.append(cc_entry) |
---|
75 | else: |
---|
76 | return |
---|
77 | |
---|
78 | new_cc_val = ', '.join(cc_list) |
---|
79 | |
---|
80 | with self.env.db_transaction as db: |
---|
81 | cursor = db.cursor() |
---|
82 | cursor.execute("UPDATE ticket SET cc=%s WHERE id=%s", |
---|
83 | (new_cc_val, ticket.id)) |
---|
84 | |
---|
85 | #Internal Functions |
---|
86 | |
---|
87 | def _append_CC_radio(self, req, ticket): |
---|
88 | notify_reporter = self.env.config.getbool('notification', |
---|
89 | 'always_notify_reporter') |
---|
90 | notify_owner = self.env.config.getbool('notification', |
---|
91 | 'always_notify_owner') |
---|
92 | notify_updater = self.env.config.getbool('notification', |
---|
93 | 'always_notify_updater') |
---|
94 | |
---|
95 | self.log.info("Appending CC Radio Button to ticket %s for user %s" % (ticket.id, req.authname)) |
---|
96 | |
---|
97 | add_checked = False |
---|
98 | remove_checked = False |
---|
99 | disabled = False |
---|
100 | notemsg = None |
---|
101 | |
---|
102 | if notify_reporter and ticket.values.get('reporter') == req.authname: |
---|
103 | notemsg = " (You are permanently added to the cc list because you are the reporter.)" |
---|
104 | add_checked = True |
---|
105 | disabled = True |
---|
106 | elif notify_owner and ticket.values.get('owner') == req.authname: |
---|
107 | notemsg = " (You are automatically added to the cc list because you are the owner.)" |
---|
108 | add_checked = True |
---|
109 | disabled = True |
---|
110 | elif notify_updater: |
---|
111 | if self._already_changed_ticket(req, ticket): |
---|
112 | add_checked = True |
---|
113 | disabled = True |
---|
114 | notemsg = " (You are permanently added to the cc list because you commented on the ticket.)" |
---|
115 | else: |
---|
116 | notemsg = " Note: Updating the ticket will permanently add you to the cc list." |
---|
117 | |
---|
118 | if not add_checked: |
---|
119 | add_checked = 'cc_update_manual' in req.args and req.args['cc_update_manual'] == 'yes' |
---|
120 | remove_checked = 'cc_update_manual' in req.args and req.args['cc_update_manual'] == 'no' |
---|
121 | #If this isn't a preview with a value already selected, select the current |
---|
122 | if (not add_checked and not remove_checked): |
---|
123 | cc_action, cc_entry, cc_list = self._tweak_cc(req, ticket['cc'], True) |
---|
124 | if cc_action != 'add': |
---|
125 | add_checked = True |
---|
126 | else: |
---|
127 | remove_checked = True |
---|
128 | |
---|
129 | return tag.p("Cc me to this ticket: ", |
---|
130 | tag.input(type="radio", id="field-cc-manual1", name="cc_update_manual", |
---|
131 | value="yes", checked=(add_checked or None), disabled=(disabled or None)), |
---|
132 | tag.label("Yes", for_="field-cc-manual1"), " ", |
---|
133 | tag.input(type="radio", id="field-cc-manual0", name="cc_update_manual", |
---|
134 | value="no", checked=(remove_checked or None), disabled=(disabled or None)), |
---|
135 | tag.label("No", for_="field-cc-manual0"), |
---|
136 | notemsg and tag.em(notemsg) or "") |
---|
137 | |
---|
138 | def _already_changed_ticket(self, req, ticket): |
---|
139 | with self.env.db_query as db: |
---|
140 | cursor = db.cursor() |
---|
141 | cursor.execute("SELECT DISTINCT author,ticket FROM ticket_change " |
---|
142 | "WHERE ticket=%s AND author=%s", (ticket.id, req.authname)) |
---|
143 | for author,ticket in cursor: |
---|
144 | return True |
---|
145 | |
---|
146 | return False |
---|
147 | |
---|
148 | def _tweak_cc(self, req, cc, cc_me): |
---|
149 | """Return an (action, recipient, cc_list) tuple corresponding to a change |
---|
150 | of CC status for this user relative to the current `cc_list`.""" |
---|
151 | entries = [] |
---|
152 | email = req.session.get('email', '').strip() |
---|
153 | if req.authname != 'anonymous': |
---|
154 | entries.append(req.authname) |
---|
155 | elif email: |
---|
156 | entries.append(email) |
---|
157 | else: |
---|
158 | author = get_reporter_id(req, 'author').strip() |
---|
159 | if author and author != 'anonymous': |
---|
160 | email = author.split()[-1] |
---|
161 | if (email[0], email[-1]) == ('<', '>'): |
---|
162 | email = email[1:-1] |
---|
163 | entries.append(email) |
---|
164 | |
---|
165 | add = [] |
---|
166 | remove = [] |
---|
167 | cc_list = Chrome(self.env).cc_list(cc) |
---|
168 | for entry in entries: |
---|
169 | if entry in cc_list: |
---|
170 | if not cc_me: |
---|
171 | remove.append(entry) |
---|
172 | else: |
---|
173 | if cc_me: |
---|
174 | add.append(entry) |
---|
175 | |
---|
176 | action = entry = '' |
---|
177 | if remove: |
---|
178 | action, entry = ('remove', remove[0]) |
---|
179 | elif add: |
---|
180 | action, entry = ('add', add[0]) |
---|
181 | return (action, entry, cc_list) |
---|
182 | |
---|