]> git.ozlabs.org Git - patchwork/blob - apps/patchwork/views/xmlrpc.py
Add 'update' method to pwclient
[patchwork] / apps / patchwork / views / xmlrpc.py
1 # Patchwork - automated patch tracking system
2 # Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org>
3 #
4 # This file is part of the Patchwork package.
5 #
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.
10 #
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.
15 #
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
19 #
20 # Patchwork XMLRPC interface
21 #
22
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
32
33 import sys
34 import base64
35 import xmlrpclib
36
37 class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher):
38     def __init__(self):
39         if sys.version_info[:3] >= (2,5,):
40             SimpleXMLRPCDispatcher.__init__(self, allow_none=False,
41                     encoding=None)
42         else:
43             SimpleXMLRPCDispatcher.__init__(self)
44
45         # map of name => (auth, func)
46         self.func_map = {}
47
48
49     def register_function(self, fn, auth_required):
50         self.func_map[fn.__name__] = (auth_required, fn)
51
52
53     def _user_for_request(self, request):
54         if not request.META.has_key('HTTP_AUTHORIZATION'):
55             raise Exception("No authentication credentials given")
56
57         str = request.META.get('HTTP_AUTHORIZATION').strip()
58         if not str.startswith('Basic '):
59             raise Exception("Authentication scheme not supported")
60
61         str = str[len('Basic '):].strip()
62
63         try:
64             decoded = base64.decodestring(str)
65             username, password = decoded.split(':', 1)
66         except:
67             raise Exception("Invalid authentication credentials")
68
69         return authenticate(username = username, password = password)
70
71
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)
75
76         auth_required, fn = self.func_map[method]
77
78         if auth_required:
79             user = self._user_for_request(request)
80             if not user:
81                 raise Exception("Invalid username/password")
82
83             params = (user,) + params
84
85         return fn(*params)
86
87     def _marshaled_dispatch(self, request):
88         try:
89             params, method = xmlrpclib.loads(request.raw_post_data)
90
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)
99         except:
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,
104                 )
105
106         return response
107
108 dispatcher = PatchworkXMLRPCDispatcher()
109
110 # XMLRPC view function
111 def xmlrpc(request):
112     if request.method != 'POST':
113         return HttpResponseRedirect(
114                 urlresolvers.reverse('patchwork.views.help',
115                     kwargs = {'path': 'pwclient/'}))
116
117     response = HttpResponse()
118     try:
119         ret = dispatcher._marshaled_dispatch(request)
120         response.write(ret)
121     except Exception, e:
122         return HttpResponseServerError()
123
124     return response
125
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):
129     def wrap(f):
130         dispatcher.register_function(f, login_required)
131         return f
132
133     return wrap
134
135
136
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" ]
141
142 #######################################################################
143 # Helper functions
144 #######################################################################
145
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."""
149     return \
150         {
151          'id'           : obj.id,
152          'linkname'     : obj.linkname,
153          'name'         : obj.name,
154         }
155
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."""
159     return \
160         {
161          'id'           : obj.id,
162          'email'        : obj.email,
163          'name'         : obj.name,
164          'user'         : str(obj.user),
165         }
166
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."""
170     return \
171         {
172          'id'           : obj.id,
173          'date'         : str(obj.date),
174          'filename'     : obj.filename(),
175          'msgid'        : obj.msgid,
176          'name'         : obj.name,
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, ''),
186         }
187
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."""
191     return \
192         {
193          'id'           : obj.id,
194          'name'         : obj.name,
195          'n_patches'    : obj.n_patches(),
196          'public_url'   : obj.public_url(),
197         }
198
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."""
202     return \
203         {
204          'id'           : obj.id,
205          'name'         : obj.name,
206         }
207
208 #######################################################################
209 # Public XML-RPC methods
210 #######################################################################
211
212 @xmlrpc_method(False)
213 def pw_rpc_version():
214     """Return Patchwork XML-RPC interface version."""
215     return 1
216
217 @xmlrpc_method(False)
218 def project_list(search_str="", max_count=0):
219     """Get a list of projects matching the given filters."""
220     try:
221         if len(search_str) > 0:
222             projects = Project.objects.filter(linkname__icontains = search_str)
223         else:
224             projects = Project.objects.all()
225
226         if max_count > 0:
227             return map(project_to_dict, projects)[:max_count]
228         else:
229             return map(project_to_dict, projects)
230     except:
231         return []
232
233 @xmlrpc_method(False)
234 def project_get(project_id):
235     """Return structure for the given project ID."""
236     try:
237         project = Project.objects.filter(id = project_id)[0]
238         return project_to_dict(project)
239     except:
240         return {}
241
242 @xmlrpc_method(False)
243 def person_list(search_str="", max_count=0):
244     """Get a list of Person objects matching the given filters."""
245     try:
246         if len(search_str) > 0:
247             people = (Person.objects.filter(name__icontains = search_str) |
248                 Person.objects.filter(email__icontains = search_str))
249         else:
250             people = Person.objects.all()
251
252         if max_count > 0:
253             return map(person_to_dict, people)[:max_count]
254         else:
255             return map(person_to_dict, people)
256
257     except:
258         return []
259
260 @xmlrpc_method(False)
261 def person_get(person_id):
262     """Return structure for the given person ID."""
263     try:
264         person = Person.objects.filter(id = person_id)[0]
265         return person_to_dict(person)
266     except:
267         return {}
268
269 @xmlrpc_method(False)
270 def patch_list(filter={}):
271     """Get a list of patches matching the given filters."""
272     try:
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
275         # XML-RPC.
276         ok_fields = [
277             "id",
278             "name",
279             "project_id",
280             "submitter_id",
281             "delegate_id",
282             "state_id",
283             "date",
284             "commit_ref",
285             "hash",
286             "msgid",
287             "name",
288             "max_count",
289             ]
290
291         dfilter = {}
292         max_count = 0
293
294         for key in filter:
295             parts = key.split("__")
296             if parts[0] not in ok_fields:
297                 # Invalid field given
298                 return []
299             if len(parts) > 1:
300                 if LOOKUP_TYPES.count(parts[1]) == 0:
301                     # Invalid lookup type given
302                     return []
303
304             if parts[0] == 'project_id':
305                 dfilter['project'] = Project.objects.filter(id =
306                                         filter[key])[0]
307             elif parts[0] == 'submitter_id':
308                 dfilter['submitter'] = Person.objects.filter(id =
309                                         filter[key])[0]
310             elif parts[0] == 'state_id':
311                 dfilter['state'] = State.objects.filter(id =
312                                         filter[key])[0]
313             elif parts[0] == 'max_count':
314                 max_count = filter[key]
315             else:
316                 dfilter[key] = filter[key]
317
318         patches = Patch.objects.filter(**dfilter)
319
320         if max_count > 0:
321             return map(patch_to_dict, patches)[:max_count]
322         else:
323             return map(patch_to_dict, patches)
324
325     except:
326         return []
327
328 @xmlrpc_method(False)
329 def patch_get(patch_id):
330     """Return structure for the given patch ID."""
331     try:
332         patch = Patch.objects.filter(id = patch_id)[0]
333         return patch_to_dict(patch)
334     except:
335         return {}
336
337 @xmlrpc_method(False)
338 def patch_get_mbox(patch_id):
339     """Return mbox string for the given patch ID."""
340     try:
341         patch = Patch.objects.filter(id = patch_id)[0]
342         return patch.mbox().as_string()
343     except:
344         return ""
345
346 @xmlrpc_method(False)
347 def patch_get_diff(patch_id):
348     """Return diff for the given patch ID."""
349     try:
350         patch = Patch.objects.filter(id = patch_id)[0]
351         return patch.content
352     except:
353         return ""
354
355 @xmlrpc_method(True)
356 def patch_set(user, patch_id, params):
357     """Update a patch with the key,value pairs in params. Only some parameters
358        can be set"""
359     try:
360         ok_params = ['state', 'commit_ref', 'archived']
361
362         patch = Patch.objects.get(id = patch_id)
363
364         if not patch.is_editable(user):
365             raise Exception('No permissions to edit this patch')
366
367         for (k, v) in params.iteritems():
368             if k not in ok_params:
369                 continue
370
371             if k == 'state':
372                 patch.state = State.objects.get(id = v)
373
374             else:
375                 setattr(patch, k, v)
376
377         patch.save()
378
379         return True
380
381     except:
382         raise
383
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."""
387     try:
388         if len(search_str) > 0:
389             states = State.objects.filter(name__icontains = search_str)
390         else:
391             states = State.objects.all()
392
393         if max_count > 0:
394             return map(state_to_dict, states)[:max_count]
395         else:
396             return map(state_to_dict, states)
397     except:
398         return []
399
400 @xmlrpc_method(False)
401 def state_get(state_id):
402     """Return structure for the given state ID."""
403     try:
404         state = State.objects.filter(id = state_id)[0]
405         return state_to_dict(state)
406     except:
407         return {}