| [808] | 1 | from trac.attachment import Attachment |
|---|
| [218] | 2 | from trac.core import * |
|---|
| [229] | 3 | from tracrpc.api import IXMLRPCHandler, expose_rpc |
|---|
| [1188] | 4 | from tracrpc.util import to_timestamp |
|---|
| [218] | 5 | import trac.ticket.model as model |
|---|
| 6 | import trac.ticket.query as query |
|---|
| [1188] | 7 | from trac.ticket.api import TicketSystem |
|---|
| [1950] | 8 | from trac.ticket.notification import TicketNotifyEmail |
|---|
| [808] | 9 | |
|---|
| [1950] | 10 | import time |
|---|
| [219] | 11 | import pydoc |
|---|
| [225] | 12 | import xmlrpclib |
|---|
| [820] | 13 | from StringIO import StringIO |
|---|
| [218] | 14 | |
|---|
| [229] | 15 | class TicketRPC(Component): |
|---|
| [224] | 16 | """ An interface to Trac's ticketing system. """ |
|---|
| 17 | |
|---|
| [229] | 18 | implements(IXMLRPCHandler) |
|---|
| 19 | |
|---|
| [219] | 20 | # IXMLRPCHandler methods |
|---|
| [224] | 21 | def xmlrpc_namespace(self): |
|---|
| 22 | return 'ticket' |
|---|
| [219] | 23 | |
|---|
| [229] | 24 | def xmlrpc_methods(self): |
|---|
| 25 | yield ('TICKET_VIEW', ((list,), (list, str)), self.query) |
|---|
| [1188] | 26 | yield ('TICKET_VIEW', ((list, xmlrpclib.DateTime),), self.getRecentChanges) |
|---|
| 27 | yield ('TICKET_VIEW', ((list, int),), self.getAvailableActions) |
|---|
| [229] | 28 | yield ('TICKET_VIEW', ((list, int),), self.get) |
|---|
| [1950] | 29 | yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create) |
|---|
| 30 | yield ('TICKET_APPEND', ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update) |
|---|
| [229] | 31 | yield ('TICKET_ADMIN', ((None, int),), self.delete) |
|---|
| 32 | yield ('TICKET_VIEW', ((dict, int), (dict, int, int)), self.changeLog) |
|---|
| [808] | 33 | yield ('TICKET_VIEW', ((list, int),), self.listAttachments) |
|---|
| 34 | yield ('TICKET_VIEW', ((xmlrpclib.Binary, int, str),), self.getAttachment) |
|---|
| [820] | 35 | yield ('TICKET_APPEND', |
|---|
| [1154] | 36 | ((str, int, str, str, xmlrpclib.Binary, bool), |
|---|
| 37 | (str, int, str, str, xmlrpclib.Binary)), |
|---|
| [820] | 38 | self.putAttachment) |
|---|
| 39 | yield ('TICKET_ADMIN', ((bool, int, str),), self.deleteAttachment) |
|---|
| [1278] | 40 | yield ('TICKET_VIEW', ((list,),), self.getTicketFields) |
|---|
| [229] | 41 | |
|---|
| [225] | 42 | # Exported methods |
|---|
| [1070] | 43 | def query(self, req, qstr='status!=closed'): |
|---|
| [226] | 44 | """ Perform a ticket query, returning a list of ticket ID's. """ |
|---|
| [218] | 45 | q = query.Query.from_string(self.env, qstr) |
|---|
| 46 | out = [] |
|---|
| [1070] | 47 | for t in q.execute(req): |
|---|
| [226] | 48 | out.append(t['id']) |
|---|
| [218] | 49 | return out |
|---|
| 50 | |
|---|
| [1188] | 51 | def getRecentChanges(self, req, since): |
|---|
| 52 | """Returns a list of IDs of tickets that have changed since timestamp.""" |
|---|
| 53 | since = to_timestamp(since) |
|---|
| 54 | db = self.env.get_db_cnx() |
|---|
| 55 | cursor = db.cursor() |
|---|
| 56 | cursor.execute('SELECT id FROM ticket' |
|---|
| 57 | ' WHERE changetime >= %s', (since,)) |
|---|
| 58 | result = [] |
|---|
| 59 | for row in cursor: |
|---|
| 60 | result.append(int(row[0])) |
|---|
| 61 | return result |
|---|
| 62 | |
|---|
| 63 | def getAvailableActions(self, req, id): |
|---|
| 64 | """Returns the actions that can be performed on the ticket.""" |
|---|
| 65 | ticketSystem = TicketSystem(self.env) |
|---|
| 66 | t = model.Ticket(self.env, id) |
|---|
| 67 | return ticketSystem.get_available_actions(t, req.perm) |
|---|
| 68 | |
|---|
| [808] | 69 | def get(self, req, id): |
|---|
| [225] | 70 | """ Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """ |
|---|
| [218] | 71 | t = model.Ticket(self.env, id) |
|---|
| 72 | return (t.id, t.time_created, t.time_changed, t.values) |
|---|
| 73 | |
|---|
| [1950] | 74 | def create(self, req, summary, description, attributes = {}, notify=False): |
|---|
| [225] | 75 | """ Create a new ticket, returning the ticket ID. """ |
|---|
| [218] | 76 | t = model.Ticket(self.env) |
|---|
| [808] | 77 | t['status'] = 'new' |
|---|
| [218] | 78 | t['summary'] = summary |
|---|
| 79 | t['description'] = description |
|---|
| [826] | 80 | t['reporter'] = req.authname or 'anonymous' |
|---|
| [225] | 81 | for k, v in attributes.iteritems(): |
|---|
| [220] | 82 | t[k] = v |
|---|
| [218] | 83 | t.insert() |
|---|
| [1950] | 84 | |
|---|
| 85 | if notify: |
|---|
| 86 | try: |
|---|
| 87 | tn = TicketNotifyEmail(self.env) |
|---|
| 88 | tn.notify(t, newticket=True) |
|---|
| 89 | except Exception, e: |
|---|
| 90 | self.log.exception("Failure sending notification on creation " |
|---|
| 91 | "of ticket #%s: %s" % (t.id, e)) |
|---|
| 92 | |
|---|
| [225] | 93 | return t.id |
|---|
| [218] | 94 | |
|---|
| [1950] | 95 | def update(self, req, id, comment, attributes = {}, notify=False): |
|---|
| [224] | 96 | """ Update a ticket, returning the new ticket in the same form as getTicket(). """ |
|---|
| [1950] | 97 | now = int(time.time()) |
|---|
| 98 | |
|---|
| [219] | 99 | t = model.Ticket(self.env, id) |
|---|
| [225] | 100 | for k, v in attributes.iteritems(): |
|---|
| [220] | 101 | t[k] = v |
|---|
| [826] | 102 | t.save_changes(req.authname or 'anonymous', comment) |
|---|
| [1950] | 103 | |
|---|
| 104 | if notify: |
|---|
| 105 | try: |
|---|
| 106 | tn = TicketNotifyEmail(self.env) |
|---|
| 107 | tn.notify(t, newticket=False, modtime=now) |
|---|
| 108 | except Exception, e: |
|---|
| 109 | self.log.exception("Failure sending notification on change of " |
|---|
| 110 | "ticket #%s: %s" % (t.id, e)) |
|---|
| 111 | |
|---|
| [820] | 112 | return self.get(req, t.id) |
|---|
| [219] | 113 | |
|---|
| [808] | 114 | def delete(self, req, id): |
|---|
| [219] | 115 | """ Delete ticket with the given id. """ |
|---|
| 116 | t = model.Ticket(self.env, id) |
|---|
| 117 | t.delete() |
|---|
| 118 | |
|---|
| [820] | 119 | def changeLog(self, req, id, when=0): |
|---|
| [1732] | 120 | t = model.Ticket(self.env, id) |
|---|
| 121 | return t.get_changelog(when) |
|---|
| [219] | 122 | # Use existing documentation from Ticket model |
|---|
| [225] | 123 | changeLog.__doc__ = pydoc.getdoc(model.Ticket.get_changelog) |
|---|
| 124 | |
|---|
| [808] | 125 | def listAttachments(self, req, ticket): |
|---|
| [1154] | 126 | """ Lists attachments for a given ticket. Returns (filename, |
|---|
| 127 | description, size, time, author) for each attachment.""" |
|---|
| 128 | for t in Attachment.select(self.env, 'ticket', ticket): |
|---|
| 129 | yield (t.filename, t.description or '', t.size, t.time, t.author) |
|---|
| [225] | 130 | |
|---|
| [808] | 131 | def getAttachment(self, req, ticket, filename): |
|---|
| 132 | """ returns the content of an attachment. """ |
|---|
| 133 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
|---|
| 134 | return xmlrpclib.Binary(attachment.open().read()) |
|---|
| 135 | |
|---|
| [1154] | 136 | def putAttachment(self, req, ticket, filename, description, data, replace=True): |
|---|
| [820] | 137 | """ Add an attachment, optionally (and defaulting to) overwriting an |
|---|
| 138 | existing one. Returns filename.""" |
|---|
| 139 | if not model.Ticket(self.env, ticket).exists: |
|---|
| [808] | 140 | raise TracError, 'Ticket "%s" does not exist' % ticket |
|---|
| [820] | 141 | if replace: |
|---|
| 142 | try: |
|---|
| 143 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
|---|
| 144 | attachment.delete() |
|---|
| 145 | except TracError: |
|---|
| 146 | pass |
|---|
| [808] | 147 | attachment = Attachment(self.env, 'ticket', ticket) |
|---|
| [820] | 148 | attachment.author = req.authname or 'anonymous' |
|---|
| [1154] | 149 | attachment.description = description |
|---|
| [808] | 150 | attachment.insert(filename, StringIO(data.data), len(data.data)) |
|---|
| [820] | 151 | return attachment.filename |
|---|
| 152 | |
|---|
| 153 | def deleteAttachment(self, req, ticket, filename): |
|---|
| 154 | """ Delete an attachment. """ |
|---|
| 155 | if not model.Ticket(self.env, ticket).exists: |
|---|
| 156 | raise TracError('Ticket "%s" does not exists' % ticket) |
|---|
| 157 | attachment = Attachment(self.env, 'ticket', ticket, filename) |
|---|
| 158 | attachment.delete() |
|---|
| [808] | 159 | return True |
|---|
| 160 | |
|---|
| [1278] | 161 | def getTicketFields(self, req): |
|---|
| 162 | """ Return a list of all ticket fields fields. """ |
|---|
| 163 | return TicketSystem(self.env).get_ticket_fields() |
|---|
| [808] | 164 | |
|---|
| [1278] | 165 | |
|---|
| [225] | 166 | def ticketModelFactory(cls, cls_attributes): |
|---|
| 167 | """ Return a class which exports an interface to trac.ticket.model.<cls>. """ |
|---|
| [229] | 168 | class TicketModelImpl(Component): |
|---|
| 169 | implements(IXMLRPCHandler) |
|---|
| 170 | |
|---|
| [225] | 171 | def xmlrpc_namespace(self): |
|---|
| 172 | return 'ticket.' + cls.__name__.lower() |
|---|
| 173 | |
|---|
| [229] | 174 | def xmlrpc_methods(self): |
|---|
| 175 | yield ('TICKET_VIEW', ((list,),), self.getAll) |
|---|
| 176 | yield ('TICKET_VIEW', ((dict, str),), self.get) |
|---|
| 177 | yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
|---|
| 178 | yield ('TICKET_ADMIN', ((None, str, dict),), self.create) |
|---|
| 179 | yield ('TICKET_ADMIN', ((None, str, dict),), self.update) |
|---|
| 180 | |
|---|
| [808] | 181 | def getAll(self, req): |
|---|
| [225] | 182 | for i in cls.select(self.env): |
|---|
| 183 | yield i.name |
|---|
| 184 | getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() |
|---|
| 185 | |
|---|
| [808] | 186 | def get(self, req, name): |
|---|
| [225] | 187 | i = cls(self.env, name) |
|---|
| 188 | attributes= {} |
|---|
| [1070] | 189 | for k, default in cls_attributes.iteritems(): |
|---|
| 190 | v = getattr(i, k) |
|---|
| 191 | if v is None: |
|---|
| 192 | v = default |
|---|
| 193 | attributes[k] = v |
|---|
| [820] | 194 | return attributes |
|---|
| [225] | 195 | get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() |
|---|
| 196 | |
|---|
| [808] | 197 | def delete(self, req, name): |
|---|
| [225] | 198 | cls(self.env, name).delete() |
|---|
| 199 | delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() |
|---|
| 200 | |
|---|
| [808] | 201 | def create(self, req, name, attributes): |
|---|
| [821] | 202 | i = cls(self.env) |
|---|
| 203 | i.name = name |
|---|
| 204 | for k, v in attributes.iteritems(): |
|---|
| 205 | setattr(i, k, v) |
|---|
| 206 | i.insert(); |
|---|
| [225] | 207 | create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower() |
|---|
| 208 | |
|---|
| [808] | 209 | def update(self, req, name, attributes): |
|---|
| [225] | 210 | self._updateHelper(name, attributes).update() |
|---|
| 211 | update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower() |
|---|
| 212 | |
|---|
| 213 | def _updateHelper(self, name, attributes): |
|---|
| [821] | 214 | i = cls(self.env, name) |
|---|
| [225] | 215 | for k, v in attributes.iteritems(): |
|---|
| 216 | setattr(i, k, v) |
|---|
| 217 | return i |
|---|
| 218 | TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower() |
|---|
| 219 | TicketModelImpl.__name__ = '%sRPC' % cls.__name__ |
|---|
| 220 | return TicketModelImpl |
|---|
| 221 | |
|---|
| 222 | def ticketEnumFactory(cls): |
|---|
| 223 | """ Return a class which exports an interface to one of the Trac ticket abstract enum types. """ |
|---|
| [229] | 224 | class AbstractEnumImpl(Component): |
|---|
| 225 | implements(IXMLRPCHandler) |
|---|
| 226 | |
|---|
| [225] | 227 | def xmlrpc_namespace(self): |
|---|
| 228 | return 'ticket.' + cls.__name__.lower() |
|---|
| 229 | |
|---|
| [229] | 230 | def xmlrpc_methods(self): |
|---|
| 231 | yield ('TICKET_VIEW', ((list,),), self.getAll) |
|---|
| 232 | yield ('TICKET_VIEW', ((str, str),), self.get) |
|---|
| 233 | yield ('TICKET_ADMIN', ((None, str,),), self.delete) |
|---|
| 234 | yield ('TICKET_ADMIN', ((None, str, str),), self.create) |
|---|
| 235 | yield ('TICKET_ADMIN', ((None, str, str),), self.update) |
|---|
| 236 | |
|---|
| [808] | 237 | def getAll(self, req): |
|---|
| [225] | 238 | for i in cls.select(self.env): |
|---|
| 239 | yield i.name |
|---|
| 240 | getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower() |
|---|
| 241 | |
|---|
| [808] | 242 | def get(self, req, name): |
|---|
| [225] | 243 | i = cls(self.env, name) |
|---|
| 244 | return i.value |
|---|
| 245 | get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower() |
|---|
| 246 | |
|---|
| [808] | 247 | def delete(self, req, name): |
|---|
| [225] | 248 | cls(self.env, name).delete() |
|---|
| 249 | delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower() |
|---|
| 250 | |
|---|
| [808] | 251 | def create(self, req, name, value): |
|---|
| [820] | 252 | i = cls(self.env) |
|---|
| [821] | 253 | i.name = name |
|---|
| [820] | 254 | i.value = value |
|---|
| 255 | i.insert() |
|---|
| [225] | 256 | create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower() |
|---|
| 257 | |
|---|
| [808] | 258 | def update(self, req, name, value): |
|---|
| [225] | 259 | self._updateHelper(name, value).update() |
|---|
| 260 | update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower() |
|---|
| 261 | |
|---|
| [821] | 262 | def _updateHelper(self, name, value): |
|---|
| [820] | 263 | i = cls(self.env, name) |
|---|
| [225] | 264 | i.value = value |
|---|
| 265 | return i |
|---|
| 266 | |
|---|
| 267 | AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower() |
|---|
| 268 | AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__ |
|---|
| 269 | return AbstractEnumImpl |
|---|
| 270 | |
|---|
| [1070] | 271 | ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''}) |
|---|
| 272 | ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''}) |
|---|
| 273 | ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''}) |
|---|
| [225] | 274 | |
|---|
| 275 | ticketEnumFactory(model.Type) |
|---|
| 276 | ticketEnumFactory(model.Status) |
|---|
| 277 | ticketEnumFactory(model.Resolution) |
|---|
| 278 | ticketEnumFactory(model.Priority) |
|---|
| 279 | ticketEnumFactory(model.Severity) |
|---|