Rev. | 37 |
---|---|
Size | 18,519 bytes |
Time | 2010-09-19 15:57:18 |
Author | tag |
Log Message | tracxmlrpcを1.1.0(0.11/0.12用)におきかえた。 |
# -*- coding: utf-8 -*-
"""
License: BSD
(c) 2005-2008 ::: Alec Thomas (alec@swapoff.org)
(c) 2009 ::: www.CodeResort.com - BV Network AS (simon-code@bvnetwork.no)
"""
import inspect
from datetime import datetime
import genshi
from trac.attachment import Attachment
from trac.core import *
from trac.perm import PermissionError
from trac.resource import Resource, ResourceNotFound
import trac.ticket.model as model
import trac.ticket.query as query
from trac.ticket.api import TicketSystem
from trac.ticket.notification import TicketNotifyEmail
from trac.ticket.web_ui import TicketModule
from trac.web.chrome import add_warning
from trac.util.datefmt import to_datetime, utc
from tracrpc.api import IXMLRPCHandler, expose_rpc, Binary
from tracrpc.util import StringIO, to_utimestamp
__all__ = ['TicketRPC']
class TicketRPC(Component):
""" An interface to Trac's ticketing system. """
implements(IXMLRPCHandler)
# IXMLRPCHandler methods
def xmlrpc_namespace(self):
return 'ticket'
def xmlrpc_methods(self):
yield (None, ((list,), (list, str)), self.query)
yield (None, ((list, datetime),), self.getRecentChanges)
yield (None, ((list, int),), self.getAvailableActions)
yield (None, ((list, int),), self.getActions)
yield (None, ((list, int),), self.get)
yield ('TICKET_CREATE', ((int, str, str), (int, str, str, dict), (int, str, str, dict, bool)), self.create)
yield (None, ((list, int, str), (list, int, str, dict), (list, int, str, dict, bool)), self.update)
yield (None, ((None, int),), self.delete)
yield (None, ((dict, int), (dict, int, int)), self.changeLog)
yield (None, ((list, int),), self.listAttachments)
yield (None, ((Binary, int, str),), self.getAttachment)
yield (None,
((str, int, str, str, Binary, bool),
(str, int, str, str, Binary)),
self.putAttachment)
yield (None, ((bool, int, str),), self.deleteAttachment)
yield ('TICKET_VIEW', ((list,),), self.getTicketFields)
# Exported methods
def query(self, req, qstr='status!=closed'):
""" Perform a ticket query, returning a list of ticket ID's. """
q = query.Query.from_string(self.env, qstr)
ticket_realm = Resource('ticket')
out = []
for t in q.execute(req):
tid = t['id']
if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)):
out.append(tid)
return out
def getRecentChanges(self, req, since):
"""Returns a list of IDs of tickets that have changed since timestamp."""
since = to_utimestamp(since)
db = self.env.get_db_cnx()
cursor = db.cursor()
cursor.execute('SELECT id FROM ticket'
' WHERE changetime >= %s', (since,))
result = []
ticket_realm = Resource('ticket')
for row in cursor:
tid = int(row[0])
if 'TICKET_VIEW' in req.perm(ticket_realm(id=tid)):
result.append(tid)
return result
def getAvailableActions(self, req, id):
""" Deprecated - will be removed. Replaced by `getActions()`. """
self.log.warning("Rpc ticket.getAvailableActions is deprecated")
return [action[0] for action in self.getActions(req, id)]
def getActions(self, req, id):
"""Returns the actions that can be performed on the ticket as a list of
`[action, label, hints, [input_fields]]` elements, where `input_fields` is
a list of `[name, value, [options]]` for any required action inputs."""
ts = TicketSystem(self.env)
t = model.Ticket(self.env, id)
actions = []
for action in ts.get_available_actions(req, t):
fragment = hints = genshi.builder.Fragment()
hints = []
first_label = None
for controller in ts.action_controllers:
if action in [c_action for c_weight, c_action \
in controller.get_ticket_actions(req, t)]:
label, widget, hint = \
controller.render_ticket_action_control(req, t, action)
fragment += widget
hints.append(hint)
first_label = first_label == None and label or first_label
controls = []
for elem in fragment.children:
if not isinstance(elem, genshi.builder.Element):
continue
if elem.tag == 'input':
controls.append((elem.attrib.get('name'),
elem.attrib.get('value'), []))
elif elem.tag == 'select':
value = ''
options = []
for opt in elem.children:
if not (opt.tag == 'option' and opt.children):
continue
option = opt.children[0]
options.append(option)
if opt.attrib.get('selected'):
value = option
controls.append((elem.attrib.get('name'),
value, options))
actions.append((action, first_label, ". ".join(hints) + '.', controls))
return actions
def get(self, req, id):
""" Fetch a ticket. Returns [id, time_created, time_changed, attributes]. """
t = model.Ticket(self.env, id)
req.perm(t.resource).require('TICKET_VIEW')
return (t.id, t.time_created, t.time_changed, t.values)
def create(self, req, summary, description, attributes = {}, notify=False):
""" Create a new ticket, returning the ticket ID. """
t = model.Ticket(self.env)
t['summary'] = summary
t['description'] = description
t['reporter'] = req.authname
for k, v in attributes.iteritems():
t[k] = v
t['status'] = 'new'
t['resolution'] = ''
t.insert()
# Call ticket change listeners
ts = TicketSystem(self.env)
for listener in ts.change_listeners:
listener.ticket_created(t)
if notify:
try:
tn = TicketNotifyEmail(self.env)
tn.notify(t, newticket=True)
except Exception, e:
self.log.exception("Failure sending notification on creation "
"of ticket #%s: %s" % (t.id, e))
return t.id
def update(self, req, id, comment, attributes = {}, notify=False):
""" Update a ticket, returning the new ticket in the same form as
getTicket(). Requires a valid 'action' in attributes to support workflow. """
now = to_datetime(None, utc)
t = model.Ticket(self.env, id)
if not 'action' in attributes:
# FIXME: Old, non-restricted update - remove soon!
self.log.warning("Rpc ticket.update for ticket %d by user %s " \
"has no workflow 'action'." % (id, req.authname))
req.perm(t.resource).require('TICKET_MODIFY')
for k, v in attributes.iteritems():
t[k] = v
t.save_changes(req.authname, comment, when=now)
else:
ts = TicketSystem(self.env)
tm = TicketModule(self.env)
action = attributes.get('action')
avail_actions = ts.get_available_actions(req, t)
if not action in avail_actions:
raise TracError("Rpc: Ticket %d by %s " \
"invalid action '%s'" % (id, req.authname, action))
controllers = list(tm._get_action_controllers(req, t, action))
all_fields = [field['name'] for field in ts.get_ticket_fields()]
for k, v in attributes.iteritems():
if k in all_fields and k != 'status':
t[k] = v
# TicketModule reads req.args - need to move things there...
req.args.update(attributes)
req.args['comment'] = comment
req.args['ts'] = str(t.time_changed) # collision hack...
changes, problems = tm.get_ticket_changes(req, t, action)
for warning in problems:
add_warning(req, "Rpc ticket.update: %s" % warning)
valid = problems and False or tm._validate_ticket(req, t)
if not valid:
raise TracError(
" ".join([warning for warning in req.chrome['warnings']]))
else:
tm._apply_ticket_changes(t, changes)
self.log.debug("Rpc ticket.update save: %s" % repr(t.values))
t.save_changes(req.authname, comment, when=now)
# Apply workflow side-effects
for controller in controllers:
controller.apply_action_side_effects(req, t, action)
# Call ticket change listeners
for listener in ts.change_listeners:
listener.ticket_changed(t, comment, req.authname, t._old)
if notify:
try:
tn = TicketNotifyEmail(self.env)
tn.notify(t, newticket=False, modtime=now)
except Exception, e:
self.log.exception("Failure sending notification on change of "
"ticket #%s: %s" % (t.id, e))
return self.get(req, t.id)
def delete(self, req, id):
""" Delete ticket with the given id. """
t = model.Ticket(self.env, id)
req.perm(t.resource).require('TICKET_ADMIN')
t.delete()
ts = TicketSystem(self.env)
# Call ticket change listeners
for listener in ts.change_listeners:
listener.ticket_deleted(t)
def changeLog(self, req, id, when=0):
t = model.Ticket(self.env, id)
req.perm(t.resource).require('TICKET_VIEW')
for date, author, field, old, new, permanent in t.get_changelog(when):
yield (date, author, field, old, new, permanent)
# Use existing documentation from Ticket model
changeLog.__doc__ = inspect.getdoc(model.Ticket.get_changelog)
def listAttachments(self, req, ticket):
""" Lists attachments for a given ticket. Returns (filename,
description, size, time, author) for each attachment."""
attachments = []
for a in Attachment.select(self.env, 'ticket', ticket):
if 'ATTACHMENT_VIEW' in req.perm(a.resource):
yield (a.filename, a.description, a.size, a.date, a.author)
def getAttachment(self, req, ticket, filename):
""" returns the content of an attachment. """
attachment = Attachment(self.env, 'ticket', ticket, filename)
req.perm(attachment.resource).require('ATTACHMENT_VIEW')
return Binary(attachment.open().read())
def putAttachment(self, req, ticket, filename, description, data, replace=True):
""" Add an attachment, optionally (and defaulting to) overwriting an
existing one. Returns filename."""
if not model.Ticket(self.env, ticket).exists:
raise ResourceNotFound('Ticket "%s" does not exist' % ticket)
if replace:
try:
attachment = Attachment(self.env, 'ticket', ticket, filename)
req.perm(attachment.resource).require('ATTACHMENT_DELETE')
attachment.delete()
except TracError:
pass
attachment = Attachment(self.env, 'ticket', ticket)
req.perm(attachment.resource).require('ATTACHMENT_CREATE')
attachment.author = req.authname
attachment.description = description
attachment.insert(filename, StringIO(data.data), len(data.data))
return attachment.filename
def deleteAttachment(self, req, ticket, filename):
""" Delete an attachment. """
if not model.Ticket(self.env, ticket).exists:
raise ResourceNotFound('Ticket "%s" does not exists' % ticket)
attachment = Attachment(self.env, 'ticket', ticket, filename)
req.perm(attachment.resource).require('ATTACHMENT_DELETE')
attachment.delete()
return True
def getTicketFields(self, req):
""" Return a list of all ticket fields fields. """
return TicketSystem(self.env).get_ticket_fields()
class StatusRPC(Component):
""" An interface to Trac ticket status objects.
Note: Status is defined by workflow, and all methods except `getAll()`
are deprecated no-op methods - these will be removed later. """
implements(IXMLRPCHandler)
# IXMLRPCHandler methods
def xmlrpc_namespace(self):
return 'ticket.status'
def xmlrpc_methods(self):
yield ('TICKET_VIEW', ((list,),), self.getAll)
yield ('TICKET_VIEW', ((dict, str),), self.get)
yield ('TICKET_ADMIN', ((None, str,),), self.delete)
yield ('TICKET_ADMIN', ((None, str, dict),), self.create)
yield ('TICKET_ADMIN', ((None, str, dict),), self.update)
def getAll(self, req):
""" Returns all ticket states described by active workflow. """
return TicketSystem(self.env).get_all_status()
def get(self, req, name):
""" Deprecated no-op method. Do not use. """
# FIXME: Remove
return '0'
def delete(self, req, name):
""" Deprecated no-op method. Do not use. """
# FIXME: Remove
return 0
def create(self, req, name, attributes):
""" Deprecated no-op method. Do not use. """
# FIXME: Remove
return 0
def update(self, req, name, attributes):
""" Deprecated no-op method. Do not use. """
# FIXME: Remove
return 0
def ticketModelFactory(cls, cls_attributes):
""" Return a class which exports an interface to trac.ticket.model.<cls>. """
class TicketModelImpl(Component):
implements(IXMLRPCHandler)
def xmlrpc_namespace(self):
return 'ticket.' + cls.__name__.lower()
def xmlrpc_methods(self):
yield ('TICKET_VIEW', ((list,),), self.getAll)
yield ('TICKET_VIEW', ((dict, str),), self.get)
yield ('TICKET_ADMIN', ((None, str,),), self.delete)
yield ('TICKET_ADMIN', ((None, str, dict),), self.create)
yield ('TICKET_ADMIN', ((None, str, dict),), self.update)
def getAll(self, req):
for i in cls.select(self.env):
yield i.name
getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower()
def get(self, req, name):
i = cls(self.env, name)
attributes= {}
for k, default in cls_attributes.iteritems():
v = getattr(i, k)
if v is None:
v = default
attributes[k] = v
return attributes
get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower()
def delete(self, req, name):
cls(self.env, name).delete()
delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower()
def create(self, req, name, attributes):
i = cls(self.env)
i.name = name
for k, v in attributes.iteritems():
setattr(i, k, v)
i.insert();
create.__doc__ = """ Create a new ticket %s with the given attributes. """ % cls.__name__.lower()
def update(self, req, name, attributes):
self._updateHelper(name, attributes).update()
update.__doc__ = """ Update ticket %s with the given attributes. """ % cls.__name__.lower()
def _updateHelper(self, name, attributes):
i = cls(self.env, name)
for k, v in attributes.iteritems():
setattr(i, k, v)
return i
TicketModelImpl.__doc__ = """ Interface to ticket %s objects. """ % cls.__name__.lower()
TicketModelImpl.__name__ = '%sRPC' % cls.__name__
return TicketModelImpl
def ticketEnumFactory(cls):
""" Return a class which exports an interface to one of the Trac ticket abstract enum types. """
class AbstractEnumImpl(Component):
implements(IXMLRPCHandler)
def xmlrpc_namespace(self):
return 'ticket.' + cls.__name__.lower()
def xmlrpc_methods(self):
yield ('TICKET_VIEW', ((list,),), self.getAll)
yield ('TICKET_VIEW', ((str, str),), self.get)
yield ('TICKET_ADMIN', ((None, str,),), self.delete)
yield ('TICKET_ADMIN', ((None, str, str),), self.create)
yield ('TICKET_ADMIN', ((None, str, str),), self.update)
def getAll(self, req):
for i in cls.select(self.env):
yield i.name
getAll.__doc__ = """ Get a list of all ticket %s names. """ % cls.__name__.lower()
def get(self, req, name):
if (cls.__name__ == 'Status'):
i = cls(self.env)
x = name
else:
i = cls(self.env, name)
x = i.value
return x
get.__doc__ = """ Get a ticket %s. """ % cls.__name__.lower()
def delete(self, req, name):
cls(self.env, name).delete()
delete.__doc__ = """ Delete a ticket %s """ % cls.__name__.lower()
def create(self, req, name, value):
i = cls(self.env)
i.name = name
i.value = value
i.insert()
create.__doc__ = """ Create a new ticket %s with the given value. """ % cls.__name__.lower()
def update(self, req, name, value):
self._updateHelper(name, value).update()
update.__doc__ = """ Update ticket %s with the given value. """ % cls.__name__.lower()
def _updateHelper(self, name, value):
i = cls(self.env, name)
i.value = value
return i
AbstractEnumImpl.__doc__ = """ Interface to ticket %s. """ % cls.__name__.lower()
AbstractEnumImpl.__name__ = '%sRPC' % cls.__name__
return AbstractEnumImpl
ticketModelFactory(model.Component, {'name': '', 'owner': '', 'description': ''})
ticketModelFactory(model.Version, {'name': '', 'time': 0, 'description': ''})
ticketModelFactory(model.Milestone, {'name': '', 'due': 0, 'completed': 0, 'description': ''})
ticketEnumFactory(model.Type)
ticketEnumFactory(model.Resolution)
ticketEnumFactory(model.Priority)
ticketEnumFactory(model.Severity)