diff options
author | Jeremy Kerr <jk@ozlabs.org> | 2008-09-09 09:28:33 +1000 |
---|---|---|
committer | Jeremy Kerr <jk@ozlabs.org> | 2008-09-09 09:28:33 +1000 |
commit | a66875053f1014de3767937b1a918949c667ac74 (patch) | |
tree | f1cd259e995b8323dbfdb654c8da3f5ae3c75c12 /apps/patchwork/views/xmlrpc.py | |
parent | 18638f005afea23e13f7329ed3ebbdae8d35582c (diff) | |
download | patchwork-a66875053f1014de3767937b1a918949c667ac74.tar.bz2 patchwork-a66875053f1014de3767937b1a918949c667ac74.tar.xz |
Add 'update' method to pwclient
This requires a new xmlrpc function, 'patch_set'. To do this, we
need HTTP Authentication support, which means changing to a custom
XMLRPC distpatcher that is aware of the Authorization: header.
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
Diffstat (limited to 'apps/patchwork/views/xmlrpc.py')
-rw-r--r-- | apps/patchwork/views/xmlrpc.py | 407 |
1 files changed, 407 insertions, 0 deletions
diff --git a/apps/patchwork/views/xmlrpc.py b/apps/patchwork/views/xmlrpc.py new file mode 100644 index 0000000..f493cf7 --- /dev/null +++ b/apps/patchwork/views/xmlrpc.py @@ -0,0 +1,407 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Patchwork XMLRPC interface +# + +from django.core.exceptions import ImproperlyConfigured +from SimpleXMLRPCServer import SimpleXMLRPCDispatcher +from django.http import HttpResponse, HttpResponseRedirect, \ + HttpResponseServerError +from django.conf import settings +from django.core import urlresolvers +from django.shortcuts import render_to_response +from django.contrib.auth import authenticate +from patchwork.models import Patch, Project, Person, Bundle, State + +import sys +import base64 +import xmlrpclib + +class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher): + def __init__(self): + if sys.version_info[:3] >= (2,5,): + SimpleXMLRPCDispatcher.__init__(self, allow_none=False, + encoding=None) + else: + SimpleXMLRPCDispatcher.__init__(self) + + # map of name => (auth, func) + self.func_map = {} + + + def register_function(self, fn, auth_required): + self.func_map[fn.__name__] = (auth_required, fn) + + + def _user_for_request(self, request): + if not request.META.has_key('HTTP_AUTHORIZATION'): + raise Exception("No authentication credentials given") + + str = request.META.get('HTTP_AUTHORIZATION').strip() + if not str.startswith('Basic '): + raise Exception("Authentication scheme not supported") + + str = str[len('Basic '):].strip() + + try: + decoded = base64.decodestring(str) + username, password = decoded.split(':', 1) + except: + raise Exception("Invalid authentication credentials") + + return authenticate(username = username, password = password) + + + def _dispatch(self, request, method, params): + if method not in self.func_map.keys(): + raise Exception('method "%s" is not supported' % method) + + auth_required, fn = self.func_map[method] + + if auth_required: + user = self._user_for_request(request) + if not user: + raise Exception("Invalid username/password") + + params = (user,) + params + + return fn(*params) + + def _marshaled_dispatch(self, request): + try: + params, method = xmlrpclib.loads(request.raw_post_data) + + response = self._dispatch(request, method, params) + # wrap response in a singleton tuple + response = (response,) + response = xmlrpclib.dumps(response, methodresponse=1, + allow_none=self.allow_none, encoding=self.encoding) + except xmlrpclib.Fault, fault: + response = xmlrpclib.dumps(fault, allow_none=self.allow_none, + encoding=self.encoding) + except: + # report exception back to server + response = xmlrpclib.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)), + encoding=self.encoding, allow_none=self.allow_none, + ) + + return response + +dispatcher = PatchworkXMLRPCDispatcher() + +# XMLRPC view function +def xmlrpc(request): + if request.method != 'POST': + return HttpResponseRedirect( + urlresolvers.reverse('patchwork.views.help', + kwargs = {'path': 'pwclient/'})) + + response = HttpResponse() + try: + ret = dispatcher._marshaled_dispatch(request) + response.write(ret) + except Exception, e: + return HttpResponseServerError() + + return response + +# decorator for XMLRPC methods. Setting login_required to true will call +# the decorated function with a non-optional user as the first argument. +def xmlrpc_method(login_required = False): + def wrap(f): + dispatcher.register_function(f, login_required) + return f + + return wrap + + + +# We allow most of the Django field lookup types for remote queries +LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt", + "in", "startswith", "istartswith", "endswith", + "iendswith", "range", "year", "month", "day", "isnull" ] + +####################################################################### +# Helper functions +####################################################################### + +def project_to_dict(obj): + """Return a trimmed down dictionary representation of a Project + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'linkname' : obj.linkname, + 'name' : obj.name, + } + +def person_to_dict(obj): + """Return a trimmed down dictionary representation of a Person + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'email' : obj.email, + 'name' : obj.name, + 'user' : str(obj.user), + } + +def patch_to_dict(obj): + """Return a trimmed down dictionary representation of a Patch + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'date' : str(obj.date), + 'filename' : obj.filename(), + 'msgid' : obj.msgid, + 'name' : obj.name, + 'project' : str(obj.project), + 'project_id' : obj.project_id, + 'state' : str(obj.state), + 'state_id' : obj.state_id, + 'submitter' : str(obj.submitter), + 'submitter_id' : obj.submitter_id, + 'delegate' : str(obj.delegate), + 'delegate_id' : max(obj.delegate_id, 0), + 'commit_ref' : max(obj.commit_ref, ''), + } + +def bundle_to_dict(obj): + """Return a trimmed down dictionary representation of a Bundle + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + 'n_patches' : obj.n_patches(), + 'public_url' : obj.public_url(), + } + +def state_to_dict(obj): + """Return a trimmed down dictionary representation of a State + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + } + +####################################################################### +# Public XML-RPC methods +####################################################################### + +@xmlrpc_method(False) +def pw_rpc_version(): + """Return Patchwork XML-RPC interface version.""" + return 1 + +@xmlrpc_method(False) +def project_list(search_str="", max_count=0): + """Get a list of projects matching the given filters.""" + try: + if len(search_str) > 0: + projects = Project.objects.filter(linkname__icontains = search_str) + else: + projects = Project.objects.all() + + if max_count > 0: + return map(project_to_dict, projects)[:max_count] + else: + return map(project_to_dict, projects) + except: + return [] + +@xmlrpc_method(False) +def project_get(project_id): + """Return structure for the given project ID.""" + try: + project = Project.objects.filter(id = project_id)[0] + return project_to_dict(project) + except: + return {} + +@xmlrpc_method(False) +def person_list(search_str="", max_count=0): + """Get a list of Person objects matching the given filters.""" + try: + if len(search_str) > 0: + people = (Person.objects.filter(name__icontains = search_str) | + Person.objects.filter(email__icontains = search_str)) + else: + people = Person.objects.all() + + if max_count > 0: + return map(person_to_dict, people)[:max_count] + else: + return map(person_to_dict, people) + + except: + return [] + +@xmlrpc_method(False) +def person_get(person_id): + """Return structure for the given person ID.""" + try: + person = Person.objects.filter(id = person_id)[0] + return person_to_dict(person) + except: + return {} + +@xmlrpc_method(False) +def patch_list(filter={}): + """Get a list of patches matching the given filters.""" + try: + # We allow access to many of the fields. But, some fields are + # filtered by raw object so we must lookup by ID instead over + # XML-RPC. + ok_fields = [ + "id", + "name", + "project_id", + "submitter_id", + "delegate_id", + "state_id", + "date", + "commit_ref", + "hash", + "msgid", + "name", + "max_count", + ] + + dfilter = {} + max_count = 0 + + for key in filter: + parts = key.split("__") + if parts[0] not in ok_fields: + # Invalid field given + return [] + if len(parts) > 1: + if LOOKUP_TYPES.count(parts[1]) == 0: + # Invalid lookup type given + return [] + + if parts[0] == 'project_id': + dfilter['project'] = Project.objects.filter(id = + filter[key])[0] + elif parts[0] == 'submitter_id': + dfilter['submitter'] = Person.objects.filter(id = + filter[key])[0] + elif parts[0] == 'state_id': + dfilter['state'] = State.objects.filter(id = + filter[key])[0] + elif parts[0] == 'max_count': + max_count = filter[key] + else: + dfilter[key] = filter[key] + + patches = Patch.objects.filter(**dfilter) + + if max_count > 0: + return map(patch_to_dict, patches)[:max_count] + else: + return map(patch_to_dict, patches) + + except: + return [] + +@xmlrpc_method(False) +def patch_get(patch_id): + """Return structure for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_mbox(patch_id): + """Return mbox string for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch.mbox().as_string() + except: + return "" + +@xmlrpc_method(False) +def patch_get_diff(patch_id): + """Return diff for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch.content + except: + return "" + +@xmlrpc_method(True) +def patch_set(user, patch_id, params): + """Update a patch with the key,value pairs in params. Only some parameters + can be set""" + try: + ok_params = ['state', 'commit_ref', 'archived'] + + patch = Patch.objects.get(id = patch_id) + + if not patch.is_editable(user): + raise Exception('No permissions to edit this patch') + + for (k, v) in params.iteritems(): + if k not in ok_params: + continue + + if k == 'state': + patch.state = State.objects.get(id = v) + + else: + setattr(patch, k, v) + + patch.save() + + return True + + except: + raise + +@xmlrpc_method(False) +def state_list(search_str="", max_count=0): + """Get a list of state structures matching the given search string.""" + try: + if len(search_str) > 0: + states = State.objects.filter(name__icontains = search_str) + else: + states = State.objects.all() + + if max_count > 0: + return map(state_to_dict, states)[:max_count] + else: + return map(state_to_dict, states) + except: + return [] + +@xmlrpc_method(False) +def state_get(state_id): + """Return structure for the given state ID.""" + try: + state = State.objects.filter(id = state_id)[0] + return state_to_dict(state) + except: + return {} |