import string
import tempfile
import subprocess
+import base64
import ConfigParser
# Default Patchwork remote XML-RPC server URL
"""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 <action> [options]\n\n" % \
(os.path.basename(sys.argv[0])))
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()
submitter_str = ""
delegate_str = ""
project_str = ""
+ commit_str = ""
+ state_str = ""
url = DEFAULT_URL
config = ConfigParser.ConfigParser()
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))
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)
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()
if settings.ENABLE_XMLRPC:
urlpatterns += patterns('',
+ (r'xmlrpc/$', 'patchwork.views.xmlrpc.xmlrpc'),
(r'^pwclient.py/$', 'patchwork.views.pwclient'),
(r'^project/(?P<project_id>[^/]+)/pwclientrc/$',
'patchwork.views.pwclientrc'),
--- /dev/null
+# 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 {}
+++ /dev/null
-# Patchwork - automated patch tracking system
-# Copyright (C) 2008 Nate Case <ncase@xes-inc.com>
-#
-# 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 {}
# Set to True to enable the Patchwork XML-RPC interface
ENABLE_XMLRPC = False
-XMLRPC_METHODS = (
- # List methods to be exposed in the form (<method path>, <xml-rpcname>,)
- ('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(\
{'document_root': '/srv/patchwork/htdocs/images'}),
)
-if settings.ENABLE_XMLRPC:
- urlpatterns += patterns('',
- (r'xmlrpc/$', 'django_xmlrpc.views.handle_xmlrpc'),
- )
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.