From: Jeremy Kerr Date: Mon, 8 Sep 2008 23:28:33 +0000 (+1000) Subject: Add 'update' method to pwclient X-Git-Url: https://git.ozlabs.org/?a=commitdiff_plain;h=a66875053f1014de3767937b1a918949c667ac74;p=patchwork 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 --- diff --git a/apps/patchwork/bin/pwclient.py b/apps/patchwork/bin/pwclient.py index a50df73..0831086 100755 --- a/apps/patchwork/bin/pwclient.py +++ b/apps/patchwork/bin/pwclient.py @@ -26,6 +26,7 @@ import getopt import string import tempfile import subprocess +import base64 import ConfigParser # Default Patchwork remote XML-RPC server URL @@ -78,6 +79,24 @@ class Filter: """Return human-readable description of the filter.""" return str(self.d) +class BasicHTTPAuthTransport(xmlrpclib.Transport): + + def __init__(self, username = None, password = None): + self.username = username + self.password = password + xmlrpclib.Transport.__init__(self) + + def authenticated(self): + return self.username != None and self.password != None + + def send_host(self, connection, host): + xmlrpclib.Transport.send_host(self, connection, host) + if not self.authenticated(): + return + credentials = '%s:%s' % (self.username, self.password) + auth = 'Basic ' + base64.encodestring(credentials).strip() + connection.putheader('Authorization', auth) + def usage(): sys.stderr.write("Usage: %s [options]\n\n" % \ (os.path.basename(sys.argv[0]))) @@ -235,9 +254,39 @@ def action_apply(rpc, patch_id): sys.stderr.write("Error: No patch content found\n") sys.exit(1) +def action_update_patch(rpc, patch_id, state = None, commit = None): + patch = rpc.patch_get(patch_id) + if patch == {}: + sys.stderr.write("Error getting information on patch ID %d\n" % \ + patch_id) + sys.exit(1) + + params = {} + + if state: + state_id = state_id_by_name(rpc, state) + if state_id == 0: + sys.stderr.write("Error: No State found matching %s*\n" % state) + sys.exit(1) + params['state'] = state_id + + if commit: + params['commit_ref'] = commit + + success = False + try: + success = rpc.patch_set(patch_id, params) + except xmlrpclib.Fault, f: + sys.stderr.write("Error updating patch: %s\n" % f.faultString) + + if not success: + sys.stderr.write("Patch not updated\n") + +auth_actions = ['update'] + def main(): try: - opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:') + opts, args = getopt.getopt(sys.argv[2:], 's:p:w:d:n:c:') except getopt.GetoptError, err: print str(err) usage() @@ -252,6 +301,8 @@ def main(): submitter_str = "" delegate_str = "" project_str = "" + commit_str = "" + state_str = "" url = DEFAULT_URL config = ConfigParser.ConfigParser() @@ -266,13 +317,15 @@ def main(): for name, value in opts: if name == '-s': - filt.add("state", value) + state_str = value elif name == '-p': project_str = value elif name == '-w': submitter_str = value elif name == '-d': delegate_str = value + elif name == '-c': + commit_str = value elif name == '-n': try: filt.add("max_count", int(value)) @@ -287,11 +340,29 @@ def main(): sys.stderr.write("Too many arguments specified\n") usage() + (username, password) = (None, None) + transport = None + if action in auth_actions: + if config.has_option('auth', 'username') and \ + config.has_option('auth', 'password'): + + transport = BasicHTTPAuthTransport( \ + config.get('auth', 'username'), + config.get('auth', 'password')) + + else: + sys.stderr.write(("The %s action requires authentication, " + "but no username or password\nis configured\n") % action) + sys.exit(1) + if project_str: filt.add("project", project_str) + if state_str: + filt.add("state", state_str) + try: - rpc = xmlrpclib.Server(url) + rpc = xmlrpclib.Server(url, transport = transport) except: sys.stderr.write("Unable to connect to %s\n" % url) sys.exit(1) @@ -336,6 +407,16 @@ def main(): action_apply(rpc, patch_id) + elif action == 'update': + try: + patch_id = int(args[0]) + except: + sys.stderr.write("Invalid patch ID given\n") + sys.exit(1) + + action_update_patch(rpc, patch_id, state = state_str, + commit = commit_str) + else: sys.stderr.write("Unknown action '%s'\n" % action) usage() diff --git a/apps/patchwork/urls.py b/apps/patchwork/urls.py index f7c942a..ef1f2ad 100644 --- a/apps/patchwork/urls.py +++ b/apps/patchwork/urls.py @@ -58,6 +58,7 @@ urlpatterns = patterns('', if settings.ENABLE_XMLRPC: urlpatterns += patterns('', + (r'xmlrpc/$', 'patchwork.views.xmlrpc.xmlrpc'), (r'^pwclient.py/$', 'patchwork.views.pwclient'), (r'^project/(?P[^/]+)/pwclientrc/$', 'patchwork.views.pwclientrc'), 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 +# +# 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 {} diff --git a/apps/patchwork/xmlrpc.py b/apps/patchwork/xmlrpc.py deleted file mode 100644 index fb64a7d..0000000 --- a/apps/patchwork/xmlrpc.py +++ /dev/null @@ -1,258 +0,0 @@ -# Patchwork - automated patch tracking system -# Copyright (C) 2008 Nate Case -# -# 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 -# -# The XML-RPC interface provides a watered down, read-only interface to -# the Patchwork database. It's intended to be safe to export to the public -# Internet. A small subset of the object data is included, and the type -# of requests/queries you can do is limited by the methods -# that we export. - -from patchwork.models import Patch, Project, Person, Bundle, State - -# 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 -####################################################################### - -def pw_rpc_version(): - """Return Patchwork XML-RPC interface version.""" - return 1 - -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 [] - -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 {} - -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 [] - -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 {} - -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 [] - -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 {} - -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 "" - -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 "" - -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 [] - -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 {} diff --git a/apps/settings.py b/apps/settings.py index f70ac2a..83aeeb4 100644 --- a/apps/settings.py +++ b/apps/settings.py @@ -101,25 +101,8 @@ ACCOUNT_ACTIVATION_DAYS = 7 # Set to True to enable the Patchwork XML-RPC interface ENABLE_XMLRPC = False -XMLRPC_METHODS = ( - # List methods to be exposed in the form (, ,) - ('patchwork.xmlrpc.pw_rpc_version', 'pw_rpc_version',), - ('patchwork.xmlrpc.patch_list', 'patch_list',), - ('patchwork.xmlrpc.patch_get', 'patch_get',), - ('patchwork.xmlrpc.patch_get_mbox', 'patch_get_mbox',), - ('patchwork.xmlrpc.patch_get_diff', 'patch_get_diff',), - ('patchwork.xmlrpc.project_list', 'project_list',), - ('patchwork.xmlrpc.project_get', 'project_get',), - ('patchwork.xmlrpc.person_list', 'person_list',), - ('patchwork.xmlrpc.person_get', 'person_get',), - ('patchwork.xmlrpc.state_list', 'state_list',), - ('patchwork.xmlrpc.state_get', 'state_get',), -) - try: from local_settings import * - if ENABLE_XMLRPC: - INSTALLED_APPS = INSTALLED_APPS + ('django_xmlrpc',) except ImportError, ex: import sys sys.stderr.write(\ diff --git a/apps/urls.py b/apps/urls.py index 9886fd7..5c4ac57 100644 --- a/apps/urls.py +++ b/apps/urls.py @@ -49,7 +49,3 @@ urlpatterns = patterns('', {'document_root': '/srv/patchwork/htdocs/images'}), ) -if settings.ENABLE_XMLRPC: - urlpatterns += patterns('', - (r'xmlrpc/$', 'django_xmlrpc.views.handle_xmlrpc'), - ) diff --git a/docs/INSTALL b/docs/INSTALL index cba69d5..8f3aab9 100644 --- a/docs/INSTALL +++ b/docs/INSTALL @@ -62,17 +62,6 @@ in brackets): cd ../../apps ln -s ../lib/packages/django-registration ./registration - (OPTIONAL) If you want to enable the Patchwork XML-RPC interface, - which is required for pwclient to work, you'll need to set up the - django_xmlrpc package: - - cd lib/packages/ - wget \ - http://django-xmlrpc.googlecode.com/files/django_xmlrpc-0.1.tar.gz - tar -zxf django_xmlrpc-0.1.tar.gz - cd ../../apps - ln -s ../lib/packages/django_xmlrpc ./django_xmlrpc - The settings.py file contains default settings for patchwork, you'll need to configure settings for your own setup.