|
Lines 7-14
Link Here
|
| 7 |
import trac.ticket.query as query |
7 |
import trac.ticket.query as query |
| 8 |
from trac.ticket.api import TicketSystem |
8 |
from trac.ticket.api import TicketSystem |
| 9 |
from trac.ticket.notification import TicketNotifyEmail |
9 |
from trac.ticket.notification import TicketNotifyEmail |
|
|
10 |
from trac.ticket.web_ui import TicketModule |
| 10 |
from trac.util.datefmt import utc |
11 |
from trac.util.datefmt import utc |
| 11 |
|
12 |
|
|
|
13 |
import genshi |
| 14 |
|
| 12 |
from datetime import datetime |
15 |
from datetime import datetime |
| 13 |
import inspect |
16 |
import inspect |
| 14 |
import xmlrpclib |
17 |
import xmlrpclib |
|
Lines 27-35
Link Here
|
| 27 |
yield ('TICKET_VIEW', ((list,), (list, str)), self.query) |
30 |
yield ('TICKET_VIEW', ((list,), (list, str)), self.query) |
| 28 |
yield ('TICKET_VIEW', ((list, xmlrpclib.DateTime),), self.getRecentChanges) |
31 |
yield ('TICKET_VIEW', ((list, xmlrpclib.DateTime),), self.getRecentChanges) |
| 29 |
yield ('TICKET_VIEW', ((list, int),), self.getAvailableActions) |
32 |
yield ('TICKET_VIEW', ((list, int),), self.getAvailableActions) |
|
|
33 |
yield ('TICKET_VIEW', ((list, int),), self.getActions) |
| 30 |
yield ('TICKET_VIEW', ((list, int),), self.get) |
34 |
yield ('TICKET_VIEW', ((list, int),), self.get) |
| 31 |
yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create) |
35 |
yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create) |
| 32 |
yield ('TICKET_ADMIN', ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update) |
36 |
yield ('TICKET_VIEW', ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update) |
| 33 |
yield ('TICKET_ADMIN', ((None, int),), self.delete) |
37 |
yield ('TICKET_ADMIN', ((None, int),), self.delete) |
| 34 |
yield ('TICKET_VIEW', ((dict, int), (dict, int, int)), self.changeLog) |
38 |
yield ('TICKET_VIEW', ((dict, int), (dict, int, int)), self.changeLog) |
| 35 |
yield ('TICKET_VIEW', ((list, int),), self.listAttachments) |
39 |
yield ('TICKET_VIEW', ((list, int),), self.listAttachments) |
|
Lines 63-72
Link Here
|
| 63 |
return result |
67 |
return result |
| 64 |
|
68 |
|
| 65 |
def getAvailableActions(self, req, id): |
69 |
def getAvailableActions(self, req, id): |
| 66 |
"""Returns the actions that can be performed on the ticket.""" |
70 |
""" Deprecated - will be removed. Replaced by `getActions()`. """ |
| 67 |
ticketSystem = TicketSystem(self.env) |
71 |
self.log.warning("Rpc ticket.getAvailableActions is deprecated") |
|
|
72 |
return [action for action, inputs in self.getActions(req, id)] |
| 73 |
|
| 74 |
def getActions(self, req, id): |
| 75 |
"""Returns the actions that can be performed on the ticket as a list of |
| 76 |
`[action, label, hint, [input_fields]]` elements, where `input_fields` is a list |
| 77 |
of `[name, value, [options]]` for any required action inputs.""" |
| 78 |
ts = TicketSystem(self.env) |
| 68 |
t = model.Ticket(self.env, id) |
79 |
t = model.Ticket(self.env, id) |
| 69 |
return ticketSystem.get_available_actions(req, t) |
80 |
actions = [] |
|
|
81 |
for action in ts.get_available_actions(req, t): |
| 82 |
fragment = genshi.builder.Fragment() |
| 83 |
for controller in ts.action_controllers: |
| 84 |
if action in controller.actions.keys(): |
| 85 |
(label, control, hint) = controller.render_ticket_action_control(req, t, action) |
| 86 |
fragment += control |
| 87 |
controls = [] |
| 88 |
for elem in fragment.children: |
| 89 |
if not isinstance(elem, genshi.builder.Element): |
| 90 |
continue |
| 91 |
if elem.tag == 'input': |
| 92 |
controls.append((elem.attrib.get('name'), |
| 93 |
elem.attrib.get('value'), [])) |
| 94 |
elif elem.tag == 'select': |
| 95 |
value = '' |
| 96 |
options = [] |
| 97 |
for opt in elem.children: |
| 98 |
if not (opt.tag == 'option' and opt.children): |
| 99 |
continue |
| 100 |
option = opt.children[0] |
| 101 |
options.append(option) |
| 102 |
if opt.attrib.get('selected'): |
| 103 |
value = option |
| 104 |
controls.append((elem.attrib.get('name'), |
| 105 |
value, options)) |
| 106 |
actions.append((action, label, hint, controls)) |
| 107 |
self.log.debug('Rpc ticket.getActions for ticket %d, user %s: %s' % ( |
| 108 |
id, req.authname, repr(actions))) |
| 109 |
return actions |
| 70 |
|
110 |
|
| 71 |
def get(self, req, id): |
111 |
def get(self, req, id): |
| 72 |
""" Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ |
112 |
""" Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ |
|
Lines 77-90
Link Here
|
| 77 |
def create(self, req, summary, description, attributes = {}, notify=False): |
117 |
def create(self, req, summary, description, attributes = {}, notify=False): |
| 78 |
""" Create a new ticket, returning the ticket ID. """ |
118 |
""" Create a new ticket, returning the ticket ID. """ |
| 79 |
t = model.Ticket(self.env) |
119 |
t = model.Ticket(self.env) |
| 80 |
t['status'] = 'new' |
|
|
| 81 |
t['summary'] = summary |
120 |
t['summary'] = summary |
| 82 |
t['description'] = description |
121 |
t['description'] = description |
| 83 |
t['reporter'] = req.authname or 'anonymous' |
122 |
t['reporter'] = req.authname or 'anonymous' |
| 84 |
for k, v in attributes.iteritems(): |
123 |
for k, v in attributes.iteritems(): |
| 85 |
t[k] = v |
124 |
t[k] = v |
|
|
125 |
t['status'] = 'new' |
| 126 |
t['resolution'] = '' |
| 86 |
t.insert() |
127 |
t.insert() |
| 87 |
|
128 |
# Call ticket change listeners |
|
|
129 |
ts = TicketSystem(self.env) |
| 130 |
for listener in ts.change_listeners: |
| 131 |
listener.ticket_created(t) |
| 88 |
if notify: |
132 |
if notify: |
| 89 |
try: |
133 |
try: |
| 90 |
tn = TicketNotifyEmail(self.env) |
134 |
tn = TicketNotifyEmail(self.env) |
|
Lines 92-109
Link Here
|
| 92 |
except Exception, e: |
136 |
except Exception, e: |
| 93 |
self.log.exception("Failure sending notification on creation " |
137 |
self.log.exception("Failure sending notification on creation " |
| 94 |
"of ticket #%s: %s" % (t.id, e)) |
138 |
"of ticket #%s: %s" % (t.id, e)) |
| 95 |
|
|
|
| 96 |
return t.id |
139 |
return t.id |
| 97 |
|
140 |
|
| 98 |
def update(self, req, id, comment, attributes = {}, notify=False): |
141 |
def update(self, req, id, comment, attributes = {}, notify=False): |
| 99 |
""" Update a ticket, returning the new ticket in the same form as getTicket(). """ |
142 |
""" Update a ticket, returning the new ticket in the same form as |
|
|
143 |
getTicket(). Requires a valid 'action' in attributes to support workflow. """ |
| 100 |
now = datetime.now(utc) |
144 |
now = datetime.now(utc) |
| 101 |
|
|
|
| 102 |
t = model.Ticket(self.env, id) |
145 |
t = model.Ticket(self.env, id) |
| 103 |
for k, v in attributes.iteritems(): |
146 |
if not 'action' in attributes: |
| 104 |
t[k] = v |
147 |
# FIXME: Old, non-restricted update - remove soon! |
| 105 |
t.save_changes(req.authname or 'anonymous', comment) |
148 |
self.log.warning("Rpc ticket.update for ticket %d by user %s " \ |
| 106 |
|
149 |
"has no workflow 'action'." % (id, req.authname)) |
|
|
150 |
for k, v in attributes.iteritems(): |
| 151 |
t[k] = v |
| 152 |
t.save_changes(req.authname, comment) |
| 153 |
else: |
| 154 |
ts = TicketSystem(self.env) |
| 155 |
tm = TicketModule(self.env) |
| 156 |
action = attributes.get('action') |
| 157 |
avail_actions = ts.get_available_actions(req, t) |
| 158 |
if not action in avail_actions: |
| 159 |
raise TracError("Rpc: Ticket %d by %s " \ |
| 160 |
"invalid action '%s'" % (id, req.authname, action)) |
| 161 |
controllers = list(tm._get_action_controllers(req, t, action)) |
| 162 |
all_fields = [field['name'] for field in ts.get_ticket_fields()] |
| 163 |
for k, v in attributes.iteritems(): |
| 164 |
if k in all_fields and k not in ['status', 'resolution']: |
| 165 |
t[k] = v |
| 166 |
# TicketModule reads req.args - need to move things there... |
| 167 |
req.args.update(attributes) |
| 168 |
req.args['comment'] = comment |
| 169 |
req.args['ts'] = str(t.time_changed) # collision hack... |
| 170 |
changes, problems = tm.get_ticket_changes(req, t, action) |
| 171 |
for warning in problems: |
| 172 |
req.add_warning("Rpc ticket.update: %(warning)s", |
| 173 |
warning=warning) |
| 174 |
valid = problems and False or tm._validate_ticket(req, t) |
| 175 |
if not valid: |
| 176 |
raise TracError( |
| 177 |
" ".join([warning for warning in req.chrome['warnings']])) |
| 178 |
else: |
| 179 |
tm._apply_ticket_changes(t, changes) |
| 180 |
self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) |
| 181 |
t.save_changes(req.authname, comment) |
| 182 |
# Apply workflow side-effects |
| 183 |
for controller in controllers: |
| 184 |
controller.apply_action_side_effects(req, t, action) |
| 185 |
# Call ticket change listeners |
| 186 |
for listener in ts.change_listeners: |
| 187 |
listener.ticket_changed(t, comment, req.authname, t._old) |
| 107 |
if notify: |
188 |
if notify: |
| 108 |
try: |
189 |
try: |
| 109 |
tn = TicketNotifyEmail(self.env) |
190 |
tn = TicketNotifyEmail(self.env) |
|
Lines 111-123
Link Here
|
| 111 |
except Exception, e: |
192 |
except Exception, e: |
| 112 |
self.log.exception("Failure sending notification on change of " |
193 |
self.log.exception("Failure sending notification on change of " |
| 113 |
"ticket #%s: %s" % (t.id, e)) |
194 |
"ticket #%s: %s" % (t.id, e)) |
| 114 |
|
|
|
| 115 |
return self.get(req, t.id) |
195 |
return self.get(req, t.id) |
| 116 |
|
196 |
|
| 117 |
def delete(self, req, id): |
197 |
def delete(self, req, id): |
| 118 |
""" Delete ticket with the given id. """ |
198 |
""" Delete ticket with the given id. """ |
| 119 |
t = model.Ticket(self.env, id) |
199 |
t = model.Ticket(self.env, id) |
| 120 |
t.delete() |
200 |
t.delete() |
|
|
201 |
ts = TicketSystem(self.env) |
| 202 |
# Call ticket change listeners |
| 203 |
for listener in ts.change_listeners: |
| 204 |
listener.ticket_deleted(t) |
| 121 |
|
205 |
|
| 122 |
def changeLog(self, req, id, when=0): |
206 |
def changeLog(self, req, id, when=0): |
| 123 |
t = model.Ticket(self.env, id) |
207 |
t = model.Ticket(self.env, id) |
|
Lines 167-172
Link Here
|
| 167 |
""" Return a list of all ticket fields fields. """ |
251 |
""" Return a list of all ticket fields fields. """ |
| 168 |
return TicketSystem(self.env).get_ticket_fields() |
252 |
return TicketSystem(self.env).get_ticket_fields() |
| 169 |
|
253 |
|
|
|
254 |
class StatusRPC(Component): |
| 255 |
""" An interface to Trac ticket status objects. |
| 256 |
Note: Status is defined workflows, and all methods except getAll() |
| 257 |
are deprecated no-op methods - these will be removed later. """ |
| 258 |
|
| 259 |
implements(IXMLRPCHandler) |
| 260 |
|
| 261 |
# IXMLRPCHandler methods |
| 262 |
def xmlrpc_namespace(self): |
| 263 |
return 'ticket.status' |
| 264 |
|
| 265 |
def xmlrpc_methods(self): |
| 266 |
yield ('TICKET_VIEW', ((list,),), self.getAll) |
| 267 |
yield ('TICKET_VIEW', ((dict, str),), self.get) |
| 268 |
yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
| 269 |
yield ('TICKET_ADMIN', ((None, str, dict),), self.create) |
| 270 |
yield ('TICKET_ADMIN', ((None, str, dict),), self.update) |
| 271 |
|
| 272 |
def getAll(self, req): |
| 273 |
""" Returns all ticket states described by active workflow. """ |
| 274 |
return TicketSystem(self.env).get_all_status() |
| 275 |
|
| 276 |
def get(self, req, name): |
| 277 |
""" Deprecated no-op method. Do not use. """ |
| 278 |
# FIXME: Remove |
| 279 |
return '0' |
| 280 |
|
| 281 |
def delete(self, req, name): |
| 282 |
""" Deprecated no-op method. Do not use. """ |
| 283 |
# FIXME: Remove |
| 284 |
return 0 |
| 285 |
|
| 286 |
def create(self, req, name, attributes): |
| 287 |
""" Deprecated no-op method. Do not use. """ |
| 288 |
# FIXME: Remove |
| 289 |
return 0 |
| 290 |
|
| 291 |
def update(self, req, name, attributes): |
| 292 |
""" Deprecated no-op method. Do not use. """ |
| 293 |
# FIXME: Remove |
| 294 |
return 0 |
| 170 |
|
295 |
|
| 171 |
def ticketModelFactory(cls, cls_attributes): |
296 |
def ticketModelFactory(cls, cls_attributes): |
| 172 |
""" Return a class which exports an interface to trac.ticket.model.<cls>. """ |
297 |
""" Return a class which exports an interface to trac.ticket.model.<cls>. """ |
|
Lines 283-289
Link Here
|
| 283 |
ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) |
408 |
ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) |
| 284 |
|
409 |
|
| 285 |
ticketEnumFactory(model.Type) |
410 |
ticketEnumFactory(model.Type) |
| 286 |
ticketEnumFactory(model.Status) |
|
|
| 287 |
ticketEnumFactory(model.Resolution) |
411 |
ticketEnumFactory(model.Resolution) |
| 288 |
ticketEnumFactory(model.Priority) |
412 |
ticketEnumFactory(model.Priority) |
| 289 |
ticketEnumFactory(model.Severity) |
413 |
ticketEnumFactory(model.Severity) |