1 | # -*- coding: utf-8 -*- |
---|
2 | """ |
---|
3 | License: BSD |
---|
4 | |
---|
5 | (c) 2005-2008 ::: Alec Thomas (alec@swapoff.org) |
---|
6 | (c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no) |
---|
7 | """ |
---|
8 | |
---|
9 | import inspect |
---|
10 | from datetime import datetime |
---|
11 | |
---|
12 | import genshi |
---|
13 | |
---|
14 | from trac.attachment import Attachment |
---|
15 | from trac.core import * |
---|
16 | from trac.perm import PermissionError |
---|
17 | from trac.resource import Resource, ResourceNotFound |
---|
18 | import trac.ticket.model as model |
---|
19 | import trac.ticket.query as query |
---|
20 | from trac.ticket.api import TicketSystem |
---|
21 | from trac.ticket.notification import TicketNotifyEmail |
---|
22 | from trac.ticket.web_ui import TicketModule |
---|
23 | from trac.web.chrome import add_warning |
---|
24 | from trac.util.datefmt import to_datetime, utc |
---|
25 | from trac.util.text import to_unicode |
---|
26 | |
---|
27 | from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary |
---|
28 | from tracrpc.util import StringIO, to_utimestamp, from_utimestamp |
---|
29 | |
---|
30 | __all__ = ['TicketRPC'] |
---|
31 | |
---|
32 | class TicketRPC(Component): |
---|
33 | """ An interface to Trac's ticketing system. """ |
---|
34 | |
---|
35 | implements(IXMLRPCHandler) |
---|
36 | |
---|
37 | # IXMLRPCHandler methods |
---|
38 | def xmlrpc_namespace(self): |
---|
39 | return 'ticket' |
---|
40 | |
---|
41 | def xmlrpc_methods(self): |
---|
42 | yield (None, ((list,), (list, str)), self.query) |
---|
43 | yield (None, ((list, datetime),), self.getRecentChanges) |
---|
44 | yield (None, ((list, int),), self.getAvailableActions) |
---|
45 | yield (None, ((list, int),), self.getActions) |
---|
46 | yield (None, ((list, int),), self.get) |
---|
47 | yield ('TICKET_CREATE', ((int, str, str), |
---|
48 | (int, str, str, dict), |
---|
49 | (int, str, str, dict, bool), |
---|
50 | (int, str, str, dict, bool, datetime)), |
---|
51 | self.create) |
---|
52 | yield (None, ((list, int, str), |
---|
53 | (list, int, str, dict), |
---|
54 | (list, int, str, dict, bool), |
---|
55 | (list, int, str, dict, bool, str), |
---|
56 | (list, int, str, dict, bool, str, datetime)), |
---|
57 | self.update) |
---|
58 | yield (None, ((None, int),), self.delete) |
---|
59 | yield (None, ((dict, int), (dict, int, int)), self.changeLog) |
---|
60 | yield (None, ((list, int),), self.listAttachments) |
---|
61 | yield (None, ((Binary, int, str),), self.getAttachment) |
---|
62 | yield (None, |
---|
63 | ((str, int, str, str, Binary, bool), |
---|
64 | (str, int, str, str, Binary)), |
---|
65 | self.putAttachment) |
---|
66 | yield (None, ((bool, int, str),), self.deleteAttachment) |
---|
67 | yield ('TICKET_VIEW', ((list,),), self.getTicketFields) |
---|
68 | |
---|
69 | # Exported methods |
---|
70 | def query(self, req, qstr='status!=closed'): |
---|
71 | """ |
---|
72 | Perform a ticket query, returning a list of ticket ID's. |
---|
73 | All queries will use stored settings for maximum number of results per |
---|
74 | page and paging options. Use `max=n` to define number of results to |
---|
75 | receive, and use `page=n` to page through larger result sets. Using |
---|
76 | `max=0` will turn off paging and return all results. |
---|
77 | """ |
---|
78 | q = query.Query.from_string(self.env, qstr) |
---|
79 | ticket_realm = Resource('ticket') |
---|
80 | out = [] |
---|
81 | for t in q.execute(req): |
---|
82 | tid = t['id'] |
---|
83 | if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): |
---|
84 | out.append(tid) |
---|
85 | return out |
---|
86 | |
---|
87 | def getRecentChanges(self, req, since): |
---|
88 | """Returns a list of IDs of tickets that have changed since timestamp.""" |
---|
89 | since = to_utimestamp(since) |
---|
90 | query = 'SELECT id FROM ticket WHERE changetime >= %s' |
---|
91 | if hasattr(self.env, 'db_query'): |
---|
92 | generator = self.env.db_query(query, (since,)) |
---|
93 | else: |
---|
94 | db = self.env.get_db_cnx() |
---|
95 | cursor = db.cursor() |
---|
96 | cursor.execute(query, (since,)) |
---|
97 | generator = cursor |
---|
98 | result = [] |
---|
99 | ticket_realm = Resource('ticket') |
---|
100 | for row in generator: |
---|
101 | tid = int(row[0]) |
---|
102 | if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)): |
---|
103 | result.append(tid) |
---|
104 | return result |
---|
105 | |
---|
106 | def getAvailableActions(self, req, id): |
---|
107 | """ Deprecated - will be removed. Replaced by `getActions()`. """ |
---|
108 | self.log.warning("Rpc ticket.getAvailableActions is deprecated") |
---|
109 | return [action[0] for action in self.getActions(req, id)] |
---|
110 | |
---|
111 | def getActions(self, req, id): |
---|
112 | """Returns the actions that can be performed on the ticket as a list of |
---|
113 | `[action, label, hints, [input_fields]]` elements, where `input_fields` is |
---|
114 | a list of `[name, value, [options]]` for any required action inputs.""" |
---|
115 | ts = TicketSystem(self.env) |
---|
116 | t = model.Ticket(self.env, id) |
---|
117 | actions = [] |
---|
118 | for action in ts.get_available_actions(req, t): |
---|
119 | fragment = genshi.builder.Fragment() |
---|
120 | hints = [] |
---|
121 | first_label = None |
---|
122 | for controller in ts.action_controllers: |
---|
123 | if action in [c_action for c_weight, c_action \ |
---|
124 | in controller.get_ticket_actions(req, t)]: |
---|
125 | label, widget, hint = \ |
---|
126 | controller.render_ticket_action_control(req, t, action) |
---|
127 | fragment += widget |
---|
128 | hints.append(to_unicode(hint).rstrip('.') + '.') |
---|
129 | first_label = first_label == None and label or first_label |
---|
130 | controls = [] |
---|
131 | for elem in fragment.children: |
---|
132 | if not isinstance(elem, genshi.builder.Element): |
---|
133 | continue |
---|
134 | if elem.tag == 'input': |
---|
135 | controls.append((elem.attrib.get('name'), |
---|
136 | elem.attrib.get('value'), [])) |
---|
137 | elif elem.tag == 'select': |
---|
138 | value = '' |
---|
139 | options = [] |
---|
140 | for opt in elem.children: |
---|
141 | if not (opt.tag == 'option' and opt.children): |
---|
142 | continue |
---|
143 | option = opt.children[0] |
---|
144 | options.append(option) |
---|
145 | if opt.attrib.get('selected'): |
---|
146 | value = option |
---|
147 | controls.append((elem.attrib.get('name'), |
---|
148 | value, options)) |
---|
149 | actions.append((action, first_label, " ".join(hints), controls)) |
---|
150 | return actions |
---|
151 | |
---|
152 | def get(self, req, id): |
---|
153 | """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ |
---|
154 | t = model.Ticket(self.env, id) |
---|
155 | req.perm(t.resource).require('TICKET_VIEW') |
---|
156 | t['_ts'] = str(to_utimestamp(t.time_changed)) |
---|
157 | return (t.id, t.time_created, t.time_changed, t.values) |
---|
158 | |
---|
159 | def create(self, req, summary, description, attributes={}, notify=False, when=None): |
---|
160 | """ Create a new ticket, returning the ticket ID. |
---|
161 | Overriding 'when' requires admin permission. """ |
---|
162 | t = model.Ticket(self.env) |
---|
163 | t['summary'] = summary |
---|
164 | t['description'] = description |
---|
165 | t['reporter'] = req.authname |
---|
166 | for k, v in attributes.iteritems(): |
---|
167 | t[k] = v |
---|
168 | t['status'] = 'new' |
---|
169 | t['resolution'] = '' |
---|
170 | # custom create timestamp? |
---|
171 | if when and not 'TICKET_ADMIN' in req.perm: |
---|
172 | self.log.warn("RPC ticket.create: %r not allowed to create with " |
---|
173 | "non-current timestamp (%r)", req.authname, when) |
---|
174 | when = None |
---|
175 | t.insert(when=when) |
---|
176 | if notify: |
---|
177 | try: |
---|
178 | tn = TicketNotifyEmail(self.env) |
---|
179 | tn.notify(t, newticket=True) |
---|
180 | except Exception, e: |
---|
181 | self.log.exception("Failure sending notification on creation " |
---|
182 | "of ticket #%s: %s" % (t.id, e)) |
---|
183 | return t.id |
---|
184 | |
---|
185 | def update(self, req, id, comment, attributes={}, notify=False, author='', when=None): |
---|
186 | """ Update a ticket, returning the new ticket in the same form as |
---|
187 | get(). 'New-style' call requires two additional items in attributes: |
---|
188 | (1) 'action' for workflow support (including any supporting fields |
---|
189 | as retrieved by getActions()), |
---|
190 | (2) '_ts' changetime token for detecting update collisions (as received |
---|
191 | from get() or update() calls). |
---|
192 | ''Calling update without 'action' and '_ts' changetime token is |
---|
193 | deprecated, and will raise errors in a future version.'' """ |
---|
194 | t = model.Ticket(self.env, id) |
---|
195 | # custom author? |
---|
196 | if author and not (req.authname == 'anonymous' \ |
---|
197 | or 'TICKET_ADMIN' in req.perm(t.resource)): |
---|
198 | # only allow custom author if anonymous is permitted or user is admin |
---|
199 | self.log.warn("RPC ticket.update: %r not allowed to change author " |
---|
200 | "to %r for comment on #%d", req.authname, author, id) |
---|
201 | author = '' |
---|
202 | author = author or req.authname |
---|
203 | # custom change timestamp? |
---|
204 | if when and not 'TICKET_ADMIN' in req.perm(t.resource): |
---|
205 | self.log.warn("RPC ticket.update: %r not allowed to update #%d with " |
---|
206 | "non-current timestamp (%r)", author, id, when) |
---|
207 | when = None |
---|
208 | when = when or to_datetime(None, utc) |
---|
209 | # and action... |
---|
210 | if not 'action' in attributes: |
---|
211 | # FIXME: Old, non-restricted update - remove soon! |
---|
212 | self.log.warning("Rpc ticket.update for ticket %d by user %s " \ |
---|
213 | "has no workflow 'action'." % (id, req.authname)) |
---|
214 | req.perm(t.resource).require('TICKET_MODIFY') |
---|
215 | time_changed = attributes.pop('_ts', None) |
---|
216 | if time_changed and \ |
---|
217 | str(time_changed) != str(to_utimestamp(t.time_changed)): |
---|
218 | raise TracError("Ticket has been updated since last get().") |
---|
219 | for k, v in attributes.iteritems(): |
---|
220 | t[k] = v |
---|
221 | t.save_changes(author, comment, when=when) |
---|
222 | else: |
---|
223 | ts = TicketSystem(self.env) |
---|
224 | tm = TicketModule(self.env) |
---|
225 | # TODO: Deprecate update without time_changed timestamp |
---|
226 | time_changed = attributes.pop('_ts', to_utimestamp(t.time_changed)) |
---|
227 | try: |
---|
228 | time_changed = int(time_changed) |
---|
229 | except ValueError: |
---|
230 | raise TracError("RPC ticket.update: Wrong '_ts' token " \ |
---|
231 | "in attributes (%r)." % time_changed) |
---|
232 | action = attributes.get('action') |
---|
233 | avail_actions = ts.get_available_actions(req, t) |
---|
234 | if not action in avail_actions: |
---|
235 | raise TracError("Rpc: Ticket %d by %s " \ |
---|
236 | "invalid action '%s'" % (id, req.authname, action)) |
---|
237 | controllers = list(tm._get_action_controllers(req, t, action)) |
---|
238 | all_fields = [field['name'] for field in ts.get_ticket_fields()] |
---|
239 | for k, v in attributes.iteritems(): |
---|
240 | if k in all_fields and k != 'status': |
---|
241 | t[k] = v |
---|
242 | # TicketModule reads req.args - need to move things there... |
---|
243 | req.args.update(attributes) |
---|
244 | req.args['comment'] = comment |
---|
245 | # Collision detection: 0.11+0.12 timestamp |
---|
246 | req.args['ts'] = str(from_utimestamp(time_changed)) |
---|
247 | # Collision detection: 0.13/1.0+ timestamp |
---|
248 | req.args['view_time'] = str(time_changed) |
---|
249 | changes, problems = tm.get_ticket_changes(req, t, action) |
---|
250 | for warning in problems: |
---|
251 | add_warning(req, "Rpc ticket.update: %s" % warning) |
---|
252 | valid = problems and False or tm._validate_ticket(req, t) |
---|
253 | if not valid: |
---|
254 | raise TracError( |
---|
255 | " ".join([warning for warning in req.chrome['warnings']])) |
---|
256 | else: |
---|
257 | tm._apply_ticket_changes(t, changes) |
---|
258 | self.log.debug("Rpc ticket.update save: %s" % repr(t.values)) |
---|
259 | t.save_changes(author, comment, when=when) |
---|
260 | # Apply workflow side-effects |
---|
261 | for controller in controllers: |
---|
262 | controller.apply_action_side_effects(req, t, action) |
---|
263 | if notify: |
---|
264 | try: |
---|
265 | tn = TicketNotifyEmail(self.env) |
---|
266 | tn.notify(t, newticket=False, modtime=when) |
---|
267 | except Exception, e: |
---|
268 | self.log.exception("Failure sending notification on change of " |
---|
269 | "ticket #%s: %s" % (t.id, e)) |
---|
270 | return self.get(req, t.id) |
---|
271 | |
---|
272 | def delete(self, req, id): |
---|
273 | """ Delete ticket with the given id. """ |
---|
274 | t = model.Ticket(self.env, id) |
---|
275 | req.perm(t.resource).require('TICKET_ADMIN') |
---|
276 | t.delete() |
---|
277 | |
---|
278 | def changeLog(self, req, id, when=0): |
---|
279 | t = model.Ticket(self.env, id) |
---|
280 | req.perm(t.resource).require('TICKET_VIEW') |
---|
281 | for date, author, field, old, new, permanent in t.get_changelog(when): |
---|
282 | yield (date, author, field, old, new, permanent) |
---|
283 | # Use existing documentation from Ticket model |
---|
284 | changeLog.__doc__ = inspect.getdoc(model.Ticket.get_changelog) |
---|
285 | |
---|
286 | def listAttachments(self, req, ticket): |
---|
287 | """ Lists attachments for a given ticket. Returns (filename, |
---|
288 | description, size, time, author) for each attachment.""" |
---|
289 | attachments = [] |
---|
290 | for a in Attachment.select(self.env, 'ticket', ticket): |
---|
291 | if 'ATTACHMENT_VIEW' in req.perm(a.resource): |
---|
292 | yield (a.filename, a.description, a.size, a.date, a.author) |
---|
293 | |
---|
294 | def getAttachment(self, req, ticket, filename): |
---|
295 | """ returns the content of an attachment. """ |
---|
296 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
---|
297 | req.perm(attachment.resource).require('ATTACHMENT_VIEW') |
---|
298 | return Binary(attachment.open().read()) |
---|
299 | |
---|
300 | def putAttachment(self, req, ticket, filename, description, data, replace=True): |
---|
301 | """ Add an attachment, optionally (and defaulting to) overwriting an |
---|
302 | existing one. Returns filename.""" |
---|
303 | if not model.Ticket(self.env, ticket).exists: |
---|
304 | raise ResourceNotFound('Ticket "%s" does not exist' % ticket) |
---|
305 | if replace: |
---|
306 | try: |
---|
307 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
---|
308 | req.perm(attachment.resource).require('ATTACHMENT_DELETE') |
---|
309 | attachment.delete() |
---|
310 | except TracError: |
---|
311 | pass |
---|
312 | attachment = Attachment(self.env, 'ticket', ticket) |
---|
313 | req.perm(attachment.resource).require('ATTACHMENT_CREATE') |
---|
314 | attachment.author = req.authname |
---|
315 | attachment.description = description |
---|
316 | attachment.insert(filename, StringIO(data.data), len(data.data)) |
---|
317 | return attachment.filename |
---|
318 | |
---|
319 | def deleteAttachment(self, req, ticket, filename): |
---|
320 | """ Delete an attachment. """ |
---|
321 | if not model.Ticket(self.env, ticket).exists: |
---|
322 | raise ResourceNotFound('Ticket "%s" does not exists' % ticket) |
---|
323 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
---|
324 | req.perm(attachment.resource).require('ATTACHMENT_DELETE') |
---|
325 | attachment.delete() |
---|
326 | return True |
---|
327 | |
---|
328 | def getTicketFields(self, req): |
---|
329 | """ Return a list of all ticket fields fields. """ |
---|
330 | return TicketSystem(self.env).get_ticket_fields() |
---|
331 | |
---|
332 | class StatusRPC(Component): |
---|
333 | """ An interface to Trac ticket status objects. |
---|
334 | Note: Status is defined by workflow, and all methods except `getAll()` |
---|
335 | are deprecated no-op methods - these will be removed later. """ |
---|
336 | |
---|
337 | implements(IXMLRPCHandler) |
---|
338 | |
---|
339 | # IXMLRPCHandler methods |
---|
340 | def xmlrpc_namespace(self): |
---|
341 | return 'ticket.status' |
---|
342 | |
---|
343 | def xmlrpc_methods(self): |
---|
344 | yield ('TICKET_VIEW', ((list,),), self.getAll) |
---|
345 | yield ('TICKET_VIEW', ((dict, str),), self.get) |
---|
346 | yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
---|
347 | yield ('TICKET_ADMIN', ((None, str, dict),), self.create) |
---|
348 | yield ('TICKET_ADMIN', ((None, str, dict),), self.update) |
---|
349 | |
---|
350 | def getAll(self, req): |
---|
351 | """ Returns all ticket states described by active workflow. """ |
---|
352 | return TicketSystem(self.env).get_all_status() |
---|
353 | |
---|
354 | def get(self, req, name): |
---|
355 | """ Deprecated no-op method. Do not use. """ |
---|
356 | # FIXME: Remove |
---|
357 | return '0' |
---|
358 | |
---|
359 | def delete(self, req, name): |
---|
360 | """ Deprecated no-op method. Do not use. """ |
---|
361 | # FIXME: Remove |
---|
362 | return 0 |
---|
363 | |
---|
364 | def create(self, req, name, attributes): |
---|
365 | """ Deprecated no-op method. Do not use. """ |
---|
366 | # FIXME: Remove |
---|
367 | return 0 |
---|
368 | |
---|
369 | def update(self, req, name, attributes): |
---|
370 | """ Deprecated no-op method. Do not use. """ |
---|
371 | # FIXME: Remove |
---|
372 | return 0 |
---|
373 | |
---|
374 | def ticketModelFactory(cls, cls_attributes): |
---|
375 | """ Return a class which exports an interface to trac.ticket.model.<cls>. """ |
---|
376 | class TicketModelImpl(Component): |
---|
377 | implements(IXMLRPCHandler) |
---|
378 | |
---|
379 | def xmlrpc_namespace(self): |
---|
380 | return 'ticket.' + cls.__name__.lower() |
---|
381 | |
---|
382 | def xmlrpc_methods(self): |
---|
383 | yield ('TICKET_VIEW', ((list,),), self.getAll) |
---|
384 | yield ('TICKET_VIEW', ((dict, str),), self.get) |
---|
385 | yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
---|
386 | yield ('TICKET_ADMIN', ((None, str, dict),), self.create) |
---|
387 | yield ('TICKET_ADMIN', ((None, str, dict),), self.update) |
---|
388 | |
---|
389 | def getAll(self, req): |
---|
390 | for i in cls.select(self.env): |
---|
391 | yield i.name |
---|
392 | getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() |
---|
393 | |
---|
394 | def get(self, req, name): |
---|
395 | i = cls(self.env, name) |
---|
396 | attributes= {} |
---|
397 | for k, default in cls_attributes.iteritems(): |
---|
398 | v = getattr(i, k) |
---|
399 | if v is None: |
---|
400 | v = default |
---|
401 | attributes[k] = v |
---|
402 | return attributes |
---|
403 | get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() |
---|
404 | |
---|
405 | def delete(self, req, name): |
---|
406 | cls(self.env, name).delete() |
---|
407 | delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() |
---|
408 | |
---|
409 | def create(self, req, name, attributes): |
---|
410 | i = cls(self.env) |
---|
411 | i.name = name |
---|
412 | for k, v in attributes.iteritems(): |
---|
413 | setattr(i, k, v) |
---|
414 | i.insert(); |
---|
415 | create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower() |
---|
416 | |
---|
417 | def update(self, req, name, attributes): |
---|
418 | self._updateHelper(name, attributes).update() |
---|
419 | update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower() |
---|
420 | |
---|
421 | def _updateHelper(self, name, attributes): |
---|
422 | i = cls(self.env, name) |
---|
423 | for k, v in attributes.iteritems(): |
---|
424 | setattr(i, k, v) |
---|
425 | return i |
---|
426 | TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower() |
---|
427 | TicketModelImpl.__name__ = '%sRPC' % cls.__name__ |
---|
428 | return TicketModelImpl |
---|
429 | |
---|
430 | def ticketEnumFactory(cls): |
---|
431 | """ Return a class which exports an interface to one of the Trac ticket abstract enum types. """ |
---|
432 | class AbstractEnumImpl(Component): |
---|
433 | implements(IXMLRPCHandler) |
---|
434 | |
---|
435 | def xmlrpc_namespace(self): |
---|
436 | return 'ticket.' + cls.__name__.lower() |
---|
437 | |
---|
438 | def xmlrpc_methods(self): |
---|
439 | yield ('TICKET_VIEW', ((list,),), self.getAll) |
---|
440 | yield ('TICKET_VIEW', ((str, str),), self.get) |
---|
441 | yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
---|
442 | yield ('TICKET_ADMIN', ((None, str, str),), self.create) |
---|
443 | yield ('TICKET_ADMIN', ((None, str, str),), self.update) |
---|
444 | |
---|
445 | def getAll(self, req): |
---|
446 | for i in cls.select(self.env): |
---|
447 | yield i.name |
---|
448 | getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() |
---|
449 | |
---|
450 | def get(self, req, name): |
---|
451 | if (cls.__name__ == 'Status'): |
---|
452 | i = cls(self.env) |
---|
453 | x = name |
---|
454 | else: |
---|
455 | i = cls(self.env, name) |
---|
456 | x = i.value |
---|
457 | return x |
---|
458 | get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() |
---|
459 | |
---|
460 | def delete(self, req, name): |
---|
461 | cls(self.env, name).delete() |
---|
462 | delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() |
---|
463 | |
---|
464 | def create(self, req, name, value): |
---|
465 | i = cls(self.env) |
---|
466 | i.name = name |
---|
467 | i.value = value |
---|
468 | i.insert() |
---|
469 | create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower() |
---|
470 | |
---|
471 | def update(self, req, name, value): |
---|
472 | self._updateHelper(name, value).update() |
---|
473 | update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower() |
---|
474 | |
---|
475 | def _updateHelper(self, name, value): |
---|
476 | i = cls(self.env, name) |
---|
477 | i.value = value |
---|
478 | return i |
---|
479 | |
---|
480 | AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower() |
---|
481 | AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__ |
---|
482 | return AbstractEnumImpl |
---|
483 | |
---|
484 | ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''}) |
---|
485 | ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''}) |
---|
486 | ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) |
---|
487 | |
---|
488 | ticketEnumFactory(model.Type) |
---|
489 | ticketEnumFactory(model.Resolution) |
---|
490 | ticketEnumFactory(model.Priority) |
---|
491 | ticketEnumFactory(model.Severity) |
---|