| 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 | |
|---|