1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
4 # This file is part of the Patchwork package.
6 # Patchwork is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # Patchwork is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 # GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with Patchwork; if not, write to the Free Software
18 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 # Patchwork XMLRPC interface
23 from django.core.exceptions import ImproperlyConfigured
24 from SimpleXMLRPCServer import SimpleXMLRPCDispatcher
25 from django.http import HttpResponse, HttpResponseRedirect, \
26 HttpResponseServerError
27 from django.conf import settings
28 from django.core import urlresolvers
29 from django.shortcuts import render_to_response
30 from django.contrib.auth import authenticate
31 from patchwork.models import Patch, Project, Person, Bundle, State
37 class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
39 if sys.version_info[:3] >= (2,5,):
40 SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
43 SimpleXMLRPCDispatcher.__init__(self)
45 # map of name => (auth, func)
49 def register_function(self, fn, auth_required):
50 self.func_map[fn.__name__] = (auth_required, fn)
53 def _user_for_request(self, request):
54 if not request.META.has_key('HTTP_AUTHORIZATION'):
55 raise Exception("No authentication credentials given")
57 str = request.META.get('HTTP_AUTHORIZATION').strip()
58 if not str.startswith('Basic '):
59 raise Exception("Authentication scheme not supported")
61 str = str[len('Basic '):].strip()
64 decoded = base64.decodestring(str)
65 username, password = decoded.split(':', 1)
67 raise Exception("Invalid authentication credentials")
69 return authenticate(username = username, password = password)
72 def _dispatch(self, request, method, params):
73 if method not in self.func_map.keys():
74 raise Exception('method "%s" is not supported' % method)
76 auth_required, fn = self.func_map[method]
79 user = self._user_for_request(request)
81 raise Exception("Invalid username/password")
83 params = (user,) + params
87 def _marshaled_dispatch(self, request):
89 params, method = xmlrpclib.loads(request.raw_post_data)
91 response = self._dispatch(request, method, params)
92 # wrap response in a singleton tuple
93 response = (response,)
94 response = xmlrpclib.dumps(response, methodresponse=1,
95 allow_none=self.allow_none, encoding=self.encoding)
96 except xmlrpclib.Fault, fault:
97 response = xmlrpclib.dumps(fault, allow_none=self.allow_none,
98 encoding=self.encoding)
100 # report exception back to server
101 response = xmlrpclib.dumps(
102 xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)),
103 encoding=self.encoding, allow_none=self.allow_none,
108 dispatcher = PatchworkXMLRPCDispatcher()
110 # XMLRPC view function
112 if request.method != 'POST':
113 return HttpResponseRedirect(
114 urlresolvers.reverse('patchwork.views.help',
115 kwargs = {'path': 'pwclient/'}))
117 response = HttpResponse()
119 ret = dispatcher._marshaled_dispatch(request)
122 return HttpResponseServerError()
126 # decorator for XMLRPC methods. Setting login_required to true will call
127 # the decorated function with a non-optional user as the first argument.
128 def xmlrpc_method(login_required = False):
130 dispatcher.register_function(f, login_required)
137 # We allow most of the Django field lookup types for remote queries
138 LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt",
139 "in", "startswith", "istartswith", "endswith",
140 "iendswith", "range", "year", "month", "day", "isnull" ]
142 #######################################################################
144 #######################################################################
146 def project_to_dict(obj):
147 """Return a trimmed down dictionary representation of a Project
148 object which is OK to send to the client."""
152 'linkname' : obj.linkname,
156 def person_to_dict(obj):
157 """Return a trimmed down dictionary representation of a Person
158 object which is OK to send to the client."""
164 'user' : str(obj.user),
167 def patch_to_dict(obj):
168 """Return a trimmed down dictionary representation of a Patch
169 object which is OK to send to the client."""
173 'date' : str(obj.date),
174 'filename' : obj.filename(),
177 'project' : str(obj.project),
178 'project_id' : obj.project_id,
179 'state' : str(obj.state),
180 'state_id' : obj.state_id,
181 'submitter' : str(obj.submitter),
182 'submitter_id' : obj.submitter_id,
183 'delegate' : str(obj.delegate),
184 'delegate_id' : max(obj.delegate_id, 0),
185 'commit_ref' : max(obj.commit_ref, ''),
188 def bundle_to_dict(obj):
189 """Return a trimmed down dictionary representation of a Bundle
190 object which is OK to send to the client."""
195 'n_patches' : obj.n_patches(),
196 'public_url' : obj.public_url(),
199 def state_to_dict(obj):
200 """Return a trimmed down dictionary representation of a State
201 object which is OK to send to the client."""
208 #######################################################################
209 # Public XML-RPC methods
210 #######################################################################
212 @xmlrpc_method(False)
213 def pw_rpc_version():
214 """Return Patchwork XML-RPC interface version."""
217 @xmlrpc_method(False)
218 def project_list(search_str="", max_count=0):
219 """Get a list of projects matching the given filters."""
221 if len(search_str) > 0:
222 projects = Project.objects.filter(linkname__icontains = search_str)
224 projects = Project.objects.all()
227 return map(project_to_dict, projects)[:max_count]
229 return map(project_to_dict, projects)
233 @xmlrpc_method(False)
234 def project_get(project_id):
235 """Return structure for the given project ID."""
237 project = Project.objects.filter(id = project_id)[0]
238 return project_to_dict(project)
242 @xmlrpc_method(False)
243 def person_list(search_str="", max_count=0):
244 """Get a list of Person objects matching the given filters."""
246 if len(search_str) > 0:
247 people = (Person.objects.filter(name__icontains = search_str) |
248 Person.objects.filter(email__icontains = search_str))
250 people = Person.objects.all()
253 return map(person_to_dict, people)[:max_count]
255 return map(person_to_dict, people)
260 @xmlrpc_method(False)
261 def person_get(person_id):
262 """Return structure for the given person ID."""
264 person = Person.objects.filter(id = person_id)[0]
265 return person_to_dict(person)
269 @xmlrpc_method(False)
270 def patch_list(filter={}):
271 """Get a list of patches matching the given filters."""
273 # We allow access to many of the fields. But, some fields are
274 # filtered by raw object so we must lookup by ID instead over
295 parts = key.split("__")
296 if parts[0] not in ok_fields:
297 # Invalid field given
300 if LOOKUP_TYPES.count(parts[1]) == 0:
301 # Invalid lookup type given
304 if parts[0] == 'project_id':
305 dfilter['project'] = Project.objects.filter(id =
307 elif parts[0] == 'submitter_id':
308 dfilter['submitter'] = Person.objects.filter(id =
310 elif parts[0] == 'state_id':
311 dfilter['state'] = State.objects.filter(id =
313 elif parts[0] == 'max_count':
314 max_count = filter[key]
316 dfilter[key] = filter[key]
318 patches = Patch.objects.filter(**dfilter)
321 return map(patch_to_dict, patches)[:max_count]
323 return map(patch_to_dict, patches)
328 @xmlrpc_method(False)
329 def patch_get(patch_id):
330 """Return structure for the given patch ID."""
332 patch = Patch.objects.filter(id = patch_id)[0]
333 return patch_to_dict(patch)
337 @xmlrpc_method(False)
338 def patch_get_mbox(patch_id):
339 """Return mbox string for the given patch ID."""
341 patch = Patch.objects.filter(id = patch_id)[0]
342 return patch.mbox().as_string()
346 @xmlrpc_method(False)
347 def patch_get_diff(patch_id):
348 """Return diff for the given patch ID."""
350 patch = Patch.objects.filter(id = patch_id)[0]
356 def patch_set(user, patch_id, params):
357 """Update a patch with the key,value pairs in params. Only some parameters
360 ok_params = ['state', 'commit_ref', 'archived']
362 patch = Patch.objects.get(id = patch_id)
364 if not patch.is_editable(user):
365 raise Exception('No permissions to edit this patch')
367 for (k, v) in params.iteritems():
368 if k not in ok_params:
372 patch.state = State.objects.get(id = v)
384 @xmlrpc_method(False)
385 def state_list(search_str="", max_count=0):
386 """Get a list of state structures matching the given search string."""
388 if len(search_str) > 0:
389 states = State.objects.filter(name__icontains = search_str)
391 states = State.objects.all()
394 return map(state_to_dict, states)[:max_count]
396 return map(state_to_dict, states)
400 @xmlrpc_method(False)
401 def state_get(state_id):
402 """Return structure for the given state ID."""
404 state = State.objects.filter(id = state_id)[0]
405 return state_to_dict(state)