summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorJeremy Kerr <jk@ozlabs.org>2008-08-21 09:38:06 +0800
committerJeremy Kerr <jk@ozlabs.org>2008-08-21 09:38:06 +0800
commitc561ebe710d6e6a43aa4afc6c2036a215378ce87 (patch)
tree7d4a56233ef53a0457646c47895ac5c6e7a65d31 /apps
downloadpatchwork-c561ebe710d6e6a43aa4afc6c2036a215378ce87.tar.bz2
patchwork-c561ebe710d6e6a43aa4afc6c2036a215378ce87.tar.xz
Inital commit
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
Diffstat (limited to 'apps')
-rw-r--r--apps/__init__.py0
-rwxr-xr-xapps/manage.py14
-rw-r--r--apps/patchwork/__init__.py0
-rw-r--r--apps/patchwork/bin/parsemail-batch.sh49
-rwxr-xr-xapps/patchwork/bin/parsemail.py263
-rwxr-xr-xapps/patchwork/bin/parsemail.sh28
-rw-r--r--apps/patchwork/bin/patchparser.py158
-rwxr-xr-xapps/patchwork/bin/setup.py29
-rwxr-xr-xapps/patchwork/bin/update-patchwork-status.py70
-rw-r--r--apps/patchwork/context_processors.py32
-rw-r--r--apps/patchwork/filters.py433
-rw-r--r--apps/patchwork/forms.py213
-rw-r--r--apps/patchwork/models.py362
-rw-r--r--apps/patchwork/paginator.py88
-rw-r--r--apps/patchwork/parser.py206
-rw-r--r--apps/patchwork/requestcontext.py82
-rw-r--r--apps/patchwork/sql/project.sql6
-rw-r--r--apps/patchwork/sql/state.sql20
-rw-r--r--apps/patchwork/templatetags/__init__.py0
-rw-r--r--apps/patchwork/templatetags/filter.py36
-rw-r--r--apps/patchwork/templatetags/listurl.py136
-rw-r--r--apps/patchwork/templatetags/order.py66
-rw-r--r--apps/patchwork/templatetags/patch.py65
-rw-r--r--apps/patchwork/templatetags/person.py40
-rw-r--r--apps/patchwork/templatetags/pwurl.py76
-rw-r--r--apps/patchwork/templatetags/syntax.py72
-rw-r--r--apps/patchwork/urls.py61
-rw-r--r--apps/patchwork/utils.py193
-rw-r--r--apps/patchwork/views/__init__.py90
-rw-r--r--apps/patchwork/views/base.py66
-rw-r--r--apps/patchwork/views/bundle.py158
-rw-r--r--apps/patchwork/views/patch.py180
-rw-r--r--apps/patchwork/views/user.py201
-rw-r--r--apps/settings.py94
-rw-r--r--apps/urls.py35
35 files changed, 3622 insertions, 0 deletions
diff --git a/apps/__init__.py b/apps/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/__init__.py
diff --git a/apps/manage.py b/apps/manage.py
new file mode 100755
index 0000000..1f6f0ed
--- /dev/null
+++ b/apps/manage.py
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import sys
+
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
diff --git a/apps/patchwork/__init__.py b/apps/patchwork/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/patchwork/__init__.py
diff --git a/apps/patchwork/bin/parsemail-batch.sh b/apps/patchwork/bin/parsemail-batch.sh
new file mode 100644
index 0000000..dbf81cc
--- /dev/null
+++ b/apps/patchwork/bin/parsemail-batch.sh
@@ -0,0 +1,49 @@
+#!/bin/sh
+#
+# 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_BASE="/srv/patchwork"
+
+if $# -ne 2
+then
+ echo "usage: $0 <dir>" >&2
+ exit 1
+fi
+
+mail_dir="$1"
+
+if ! -d "$mail_dir"
+then
+ echo "$mail_dir should be a directory"?&2
+ exit 1
+fi
+
+ls -1rt "$mail_dir" |
+while read line;
+do
+ echo $line
+ PYTHONPATH="$PATCHWORK_BASE/apps":"$PATCHWORK_BASE/lib/python" \
+ DJANGO_SETTINGS_MODULE=settings \
+ "$PATCHWORK_BASE/apps/patchworkbin/parsemail.py" <
+ "$mail_dir/$line"
+done
+
+
+
diff --git a/apps/patchwork/bin/parsemail.py b/apps/patchwork/bin/parsemail.py
new file mode 100755
index 0000000..d41bd92
--- /dev/null
+++ b/apps/patchwork/bin/parsemail.py
@@ -0,0 +1,263 @@
+#!/usr/bin/python
+#
+# 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
+
+import sys
+import re
+import datetime
+import time
+import operator
+from email import message_from_file
+from email.header import Header
+from email.utils import parsedate_tz, mktime_tz
+
+from patchparser import parse_patch
+from patchwork.models import Patch, Project, Person, Comment
+
+list_id_headers = ['List-ID', 'X-Mailing-List']
+
+def find_project(mail):
+ project = None
+ listid_re = re.compile('.*<([^>]+)>.*', re.S)
+
+ for header in list_id_headers:
+ if header in mail:
+ match = listid_re.match(mail.get(header))
+ if not match:
+ continue
+
+ listid = match.group(1)
+
+ try:
+ project = Project.objects.get(listid = listid)
+ break
+ except:
+ pass
+
+ return project
+
+def find_author(mail):
+
+ from_header = mail.get('From').strip()
+ (name, email) = (None, None)
+
+ # tuple of (regex, fn)
+ # - where fn returns a (name, email) tuple from the match groups resulting
+ # from re.match().groups()
+ from_res = [
+ # for "Firstname Lastname" <example@example.com> style addresses
+ (re.compile('"?(.*?)"?\s*<([^>]+)>'), (lambda g: (g[0], g[1]))),
+
+ # for example@example.com (Firstname Lastname) style addresses
+ (re.compile('"?(.*?)"?\s*\(([^\)]+)\)'), (lambda g: (g[1], g[0]))),
+
+ # everything else
+ (re.compile('(.*)'), (lambda g: (None, g[0]))),
+ ]
+
+ for regex, fn in from_res:
+ match = regex.match(from_header)
+ if match:
+ (name, email) = fn(match.groups())
+ break
+
+ if email is None:
+ raise Exception("Could not parse From: header")
+
+ email = email.strip()
+ if name is not None:
+ name = name.strip()
+
+ try:
+ person = Person.objects.get(email = email)
+ except Person.DoesNotExist:
+ person = Person(name = name, email = email)
+
+ return person
+
+def mail_date(mail):
+ t = parsedate_tz(mail.get('Date', ''))
+ if not t:
+ print "using now()"
+ return datetime.datetime.utcnow()
+ return datetime.datetime.utcfromtimestamp(mktime_tz(t))
+
+def mail_headers(mail):
+ return reduce(operator.__concat__,
+ ['%s: %s\n' % (k, Header(v, header_name = k, \
+ continuation_ws = '\t').encode()) \
+ for (k, v) in mail.items()])
+
+def find_content(project, mail):
+ patchbuf = None
+ commentbuf = ''
+
+ for part in mail.walk():
+ if part.get_content_maintype() != 'text':
+ continue
+
+ #print "\t%s, %s" % \
+ # (part.get_content_subtype(), part.get_content_charset())
+
+ charset = part.get_content_charset()
+ if not charset:
+ charset = mail.get_charset()
+ if not charset:
+ charset = 'utf-8'
+
+ payload = unicode(part.get_payload(decode=True), charset, "replace")
+
+ if part.get_content_subtype() == 'x-patch':
+ patchbuf = payload
+
+ if part.get_content_subtype() == 'plain':
+ if not patchbuf:
+ (patchbuf, c) = parse_patch(payload)
+ else:
+ c = payload
+
+ if c is not None:
+ commentbuf += c.strip() + '\n'
+
+ patch = None
+ comment = None
+
+ if patchbuf:
+ mail_headers(mail)
+ patch = Patch(name = clean_subject(mail.get('Subject')),
+ content = patchbuf, date = mail_date(mail),
+ headers = mail_headers(mail))
+
+ if commentbuf:
+ if patch:
+ cpatch = patch
+ else:
+ cpatch = find_patch_for_comment(mail)
+ if not cpatch:
+ return (None, None)
+ comment = Comment(patch = cpatch, date = mail_date(mail),
+ content = clean_content(commentbuf),
+ headers = mail_headers(mail))
+
+ return (patch, comment)
+
+def find_patch_for_comment(mail):
+ # construct a list of possible reply message ids
+ refs = []
+ if 'In-Reply-To' in mail:
+ refs.append(mail.get('In-Reply-To'))
+
+ if 'References' in mail:
+ rs = mail.get('References').split()
+ rs.reverse()
+ for r in rs:
+ if r not in refs:
+ refs.append(r)
+
+ for ref in refs:
+ patch = None
+
+ # first, check for a direct reply
+ try:
+ patch = Patch.objects.get(msgid = ref)
+ return patch
+ except Patch.DoesNotExist:
+ pass
+
+ # see if we have comments that refer to a patch
+ try:
+ comment = Comment.objects.get(msgid = ref)
+ return comment.patch
+ except Comment.DoesNotExist:
+ pass
+
+
+ return None
+
+re_re = re.compile('^(re|fwd?)[:\s]\s*', re.I)
+prefix_re = re.compile('^\[.*\]\s*')
+whitespace_re = re.compile('\s+')
+
+def clean_subject(subject):
+ subject = re_re.sub(' ', subject)
+ subject = prefix_re.sub('', subject)
+ subject = whitespace_re.sub(' ', subject)
+ return subject.strip()
+
+sig_re = re.compile('^(-{2,3} ?|_+)\n.*', re.S | re.M)
+def clean_content(str):
+ str = sig_re.sub('', str)
+ return str.strip()
+
+def main(args):
+ mail = message_from_file(sys.stdin)
+
+ # some basic sanity checks
+ if 'From' not in mail:
+ return 0
+
+ if 'Subject' not in mail:
+ return 0
+
+ if 'Message-Id' not in mail:
+ return 0
+
+ hint = mail.get('X-Patchwork-Hint', '').lower()
+ if hint == 'ignore':
+ return 0;
+
+ project = find_project(mail)
+ if project is None:
+ print "no project found"
+ return 0
+
+ msgid = mail.get('Message-Id').strip()
+
+ author = find_author(mail)
+
+ (patch, comment) = find_content(project, mail)
+
+ if patch:
+ author.save()
+ patch.submitter = author
+ patch.msgid = msgid
+ patch.project = project
+ try:
+ patch.save()
+ except Exception, ex:
+ print ex.message
+
+ if comment:
+ author.save()
+ # looks like the original constructor for Comment takes the pk
+ # when the Comment is created. reset it here.
+ if patch:
+ comment.patch = patch
+ comment.submitter = author
+ comment.msgid = msgid
+ try:
+ comment.save()
+ except Exception, ex:
+ print ex.message
+
+ return 0
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
diff --git a/apps/patchwork/bin/parsemail.sh b/apps/patchwork/bin/parsemail.sh
new file mode 100755
index 0000000..0178e18
--- /dev/null
+++ b/apps/patchwork/bin/parsemail.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+#
+# 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_BASE="/srv/patchwork"
+
+PYTHONPATH="$PATCHWORK_BASE/apps":"$PATCHWORK_BASE/lib/python" \
+ DJANGO_SETTINGS_MODULE=settings \
+ "$PATCHWORK_BASE/apps/patchworkbin/parsemail.py"
+
+exit 0
diff --git a/apps/patchwork/bin/patchparser.py b/apps/patchwork/bin/patchparser.py
new file mode 100644
index 0000000..16d1de4
--- /dev/null
+++ b/apps/patchwork/bin/patchparser.py
@@ -0,0 +1,158 @@
+#!/usr/bin/python
+#
+# 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
+
+
+import re
+
+def parse_patch(text):
+ patchbuf = ''
+ commentbuf = ''
+ buf = ''
+
+ # state specified the line we just saw, and what to expect next
+ state = 0
+ # 0: text
+ # 1: suspected patch header (diff, ====, Index:)
+ # 2: patch header line 1 (---)
+ # 3: patch header line 2 (+++)
+ # 4: patch hunk header line (@@ line)
+ # 5: patch hunk content
+ #
+ # valid transitions:
+ # 0 -> 1 (diff, ===, Index:)
+ # 0 -> 2 (---)
+ # 1 -> 2 (---)
+ # 2 -> 3 (+++)
+ # 3 -> 4 (@@ line)
+ # 4 -> 5 (patch content)
+ # 5 -> 1 (run out of lines from @@-specifed count)
+ #
+ # Suspected patch header is stored into buf, and appended to
+ # patchbuf if we find a following hunk. Otherwise, append to
+ # comment after parsing.
+
+ # line counts while parsing a patch hunk
+ lc = (0, 0)
+ hunk = 0
+
+ hunk_re = re.compile('^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@')
+
+ for line in text.split('\n'):
+ line += '\n'
+
+ if state == 0:
+ if line.startswith('diff') or line.startswith('===') \
+ or line.startswith('Index: '):
+ state = 1
+ buf += line
+
+ elif line.startswith('--- '):
+ state = 2
+ buf += line
+
+ else:
+ commentbuf += line
+
+ elif state == 1:
+ buf += line
+ if line.startswith('--- '):
+ state = 2
+
+ elif state == 2:
+ if line.startswith('+++ '):
+ state = 3
+ buf += line
+
+ elif hunk:
+ state = 1
+ buf += line
+
+ else:
+ state = 0
+ commentbuf += buf + line
+ buf = ''
+
+ elif state == 3:
+ match = hunk_re.match(line)
+ if match:
+
+ def fn(x):
+ if not x:
+ return 1
+ return int(x)
+
+ lc = map(fn, match.groups())
+
+ state = 4
+ patchbuf += buf + line
+ buf = ''
+
+ elif line.startswith('--- '):
+ patchbuf += buf + line
+ buf = ''
+ state = 2
+
+ elif hunk:
+ state = 1
+ buf += line
+
+ else:
+ state = 0
+ commentbuf += buf + line
+ buf = ''
+
+ elif state == 4 or state == 5:
+ if line.startswith('-'):
+ lc[0] -= 1
+ elif line.startswith('+'):
+ lc[1] -= 1
+ else:
+ lc[0] -= 1
+ lc[1] -= 1
+
+ patchbuf += line
+
+ if lc[0] <= 0 and lc[1] <= 0:
+ state = 3
+ hunk += 1
+ else:
+ state = 5
+
+ else:
+ raise Exception("Unknown state %d! (line '%s')" % (state, line))
+
+ commentbuf += buf
+
+ if patchbuf == '':
+ patchbuf = None
+
+ if commentbuf == '':
+ commentbuf = None
+
+ return (patchbuf, commentbuf)
+
+if __name__ == '__main__':
+ import sys
+ (patch, comment) = parse_patch(sys.stdin.read())
+ if patch:
+ print "Patch: ------\n" + patch
+ if comment:
+ print "Comment: ----\n" + comment
diff --git a/apps/patchwork/bin/setup.py b/apps/patchwork/bin/setup.py
new file mode 100755
index 0000000..7d55815
--- /dev/null
+++ b/apps/patchwork/bin/setup.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python
+#
+# 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
+
+
+from patchwork.models import UserProfile
+from django.contrib.auth.models import User
+
+# give each existing user a userprofile
+for user in User.objects.all():
+ p = UserProfile(user = user)
+ p.save()
diff --git a/apps/patchwork/bin/update-patchwork-status.py b/apps/patchwork/bin/update-patchwork-status.py
new file mode 100755
index 0000000..c774d63
--- /dev/null
+++ b/apps/patchwork/bin/update-patchwork-status.py
@@ -0,0 +1,70 @@
+#!/usr/bin/env python
+#
+# 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
+
+
+import sys
+import subprocess
+from optparse import OptionParser
+
+def commits(options, revlist):
+ cmd = ['git-rev-list', revlist]
+ proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = options.repodir)
+
+ revs = []
+
+ for line in proc.stdout.readlines():
+ revs.append(line.strip())
+
+ return revs
+
+def commit(options, rev):
+ cmd = ['git-diff', '%(rev)s^..%(rev)s' % {'rev': rev}]
+ proc = subprocess.Popen(cmd, stdout = subprocess.PIPE, cwd = options.repodir)
+
+ buf = proc.communicate()[0]
+
+ return buf
+
+
+def main(args):
+ parser = OptionParser(usage = '%prog [options] revspec')
+ parser.add_option("-p", "--project", dest = "project", action = 'store',
+ help="use project PROJECT", metavar="PROJECT")
+ parser.add_option("-d", "--dir", dest = "repodir", action = 'store',
+ help="use git repo in DIR", metavar="DIR")
+
+ (options, args) = parser.parse_args(args[1:])
+
+ if len(args) != 1:
+ parser.error("incorrect number of arguments")
+
+ revspec = args[0]
+ revs = commits(options, revspec)
+
+ for rev in revs:
+ print rev
+ print commit(options, rev)
+
+
+if __name__ == '__main__':
+ sys.exit(main(sys.argv))
+
+
diff --git a/apps/patchwork/context_processors.py b/apps/patchwork/context_processors.py
new file mode 100644
index 0000000..f4ab5a9
--- /dev/null
+++ b/apps/patchwork/context_processors.py
@@ -0,0 +1,32 @@
+# 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
+
+
+from patchwork.models import Bundle
+from patchwork.utils import order_map, get_order
+
+def bundle(request):
+ user = request.user
+ if not user.is_authenticated():
+ return {}
+ return {'bundles': Bundle.objects.filter(owner = user)}
+
+
+def patchlists(request):
+
diff --git a/apps/patchwork/filters.py b/apps/patchwork/filters.py
new file mode 100644
index 0000000..f7fb652
--- /dev/null
+++ b/apps/patchwork/filters.py
@@ -0,0 +1,433 @@
+# 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
+
+
+from patchwork.models import Person, State
+from django.utils.safestring import mark_safe
+from django.utils.html import escape
+from django.contrib.auth.models import User
+
+class Filter(object):
+ def __init__(self, filters):
+ self.filters = filters
+ self.applied = False
+ self.forced = False
+
+ def name(self):
+ """The 'name' of the filter, to be displayed in the filter UI"""
+ return self.name
+
+ def condition(self):
+ """The current condition of the filter, to be displayed in the
+ filter UI"""
+ return self.key
+
+ def key(self):
+ """The key for this filter, to appear in the querystring. A key of
+ None will remove the param=ley pair from the querystring."""
+ return None
+
+ def set_status(self, *kwargs):
+ """Views can call this to force a specific filter status. For example,
+ a user's todo page needs to setup the delegate filter to show
+ that user's delegated patches"""
+ pass
+
+ def parse(self, dict):
+ if self.param not in dict.keys():
+ return
+ self._set_key(dict[self.param])
+
+ def url_without_me(self):
+ return self.filters.querystring_without_filter(self)
+
+ def form_function(self):
+ return 'function(form) { return "unimplemented" }'
+
+ def form(self):
+ if self.forced:
+ return mark_safe('<input type="hidden" value="%s">%s' % (self.param,
+ self.condition()))
+ return self.condition()
+ return self._form()
+
+ def kwargs(self):
+ return {}
+
+ def __str__(self):
+ return '%s: %s' % (self.name, self.kwargs())
+
+
+class SubmitterFilter(Filter):
+ param = 'submitter'
+ def __init__(self, filters):
+ super(SubmitterFilter, self).__init__(filters)
+ self.name = 'Submitter'
+ self.person = None
+ self.person_match = None
+
+ def _set_key(self, str):
+ self.person = None
+ self.person_match = None
+ submitter_id = None
+ try:
+ submitter_id = int(str)
+ except ValueError:
+ pass
+ except:
+ return
+
+ if submitter_id:
+ self.person = Person.objects.get(id = int(str))
+ self.applied = True
+ return
+
+
+ people = Person.objects.filter(name__icontains = str)
+
+ if not people:
+ return
+
+ self.person_match = str
+ self.applied = True
+
+ def kwargs(self):
+ if self.person:
+ user = self.person.user
+ if user:
+ return {'submitter__in':
+ Person.objects.filter(user = user).values('pk').query}
+ return {'submitter': self.person}
+
+ if self.person_match:
+ return {'submitter__name__icontains': self.person_match}
+ return {}
+
+ def condition(self):
+ if self.person:
+ return self.person.name
+ elif self.person_match:
+ return self.person_match
+ return ''
+
+ def _form(self):
+ name = ''
+ if self.person:
+ name = self.person.name
+ return mark_safe(('<input onKeyUp="submitter_field_change(this)" ' +
+ 'name="submitter" id="submitter_input" ' +
+ 'value="%s">&nbsp;' % escape(name)) +
+ '<select id="submitter_select" ' +
+ 'disabled="true"></select>')
+
+ def key(self):
+ if self.person:
+ return self.person.id
+ return self.person_match
+
+class StateFilter(Filter):
+ param = 'state'
+ def __init__(self, filters):
+ super(StateFilter, self).__init__(filters)
+ self.name = 'State'
+ self.state = None
+
+ def _set_key(self, str):
+ try:
+ self.state = State.objects.get(id=int(str))
+ except:
+ return
+
+ self.applied = True
+
+ def kwargs(self):
+ return {'state': self.state}
+
+ def condition(self):
+ return self.state.name
+
+ def key(self):
+ if self.state is None:
+ return None
+ return self.state.id
+
+ def _form(self):
+ str = '<select name="%s">' % self.param
+ str += '<option value="">any</option>'
+ for state in State.objects.all():
+ selected = ''
+ if self.state and self.state == state:
+ selected = ' selected="true"'
+
+ str += '<option value="%d" %s>%s</option>' % \
+ (state.id, selected, state.name)
+ str += '</select>'
+ return mark_safe(str);
+
+ def form_function(self):
+ return 'function(form) { return form.x.value }'
+
+class SearchFilter(Filter):
+ param = 'q'
+ def __init__(self, filters):
+ super(SearchFilter, self).__init__(filters)
+ self.name = 'Search'
+ self.param = 'q'
+ self.search = None
+
+ def _set_key(self, str):
+ str = str.strip()
+ if str == '':
+ return
+ self.search = str
+ self.applied = True
+
+ def kwargs(self):
+ return {'name__icontains': self.search}
+
+ def condition(self):
+ return self.search
+
+ def key(self):
+ return self.search
+
+ def _form(self):
+ value = ''
+ if self.search:
+ value = escape(self.search)
+ return mark_safe('<input name="%s" value="%s">' %\
+ (self.param, value))
+
+ def form_function(self):
+ return mark_safe('function(form) { return form.x.value }')
+
+class ArchiveFilter(Filter):
+ param = 'archive'
+ def __init__(self, filters):
+ super(ArchiveFilter, self).__init__(filters)
+ self.name = 'Archived'
+ self.archive_state = False
+ self.applied = True
+ self.param_map = {
+ True: 'true',
+ False: '',
+ None: 'both'
+ }
+ self.description_map = {
+ True: 'Yes',
+ False: 'No',
+ None: 'Both'
+ }
+
+ def _set_key(self, str):
+ self.archive_state = False
+ self.applied = True
+ for (k, v) in self.param_map.iteritems():
+ if str == v:
+ self.archive_state = k
+ if self.archive_state == None:
+ self.applied = False
+
+ def kwargs(self):
+ if self.archive_state == None:
+ return {}
+ return {'archived': self.archive_state}
+
+ def condition(self):
+ return self.description_map[self.archive_state]
+
+ def key(self):
+ if self.archive_state == False:
+ return None
+ return self.param_map[self.archive_state]
+
+ def _form(self):
+ s = ''
+ for b in [False, True, None]:
+ label = self.description_map[b]
+ selected = ''
+ if self.archive_state == b:
+ selected = 'checked="true"'
+ s += ('<input type="radio" name="%(param)s" ' + \
+ '%(selected)s value="%(value)s">%(label)s' + \
+ '&nbsp;&nbsp;&nbsp;&nbsp;') % \
+ {'label': label,
+ 'param': self.param,
+ 'selected': selected,
+ 'value': self.param_map[b]
+ }
+ return mark_safe(s)
+
+ def url_without_me(self):
+ qs = self.filters.querystring_without_filter(self)
+ if qs != '?':
+ qs += '&'
+ return qs + 'archive=both'
+
+
+class DelegateFilter(Filter):
+ param = 'delegate'
+ AnyDelegate = 1
+
+ def __init__(self, filters):
+ super(DelegateFilter, self).__init__(filters)
+ self.name = 'Delegate'
+ self.param = 'delegate'
+
+ # default to applied, but no delegate - this will result in patches with
+ # no delegate
+ self.delegate = None
+ self.applied = True
+
+ def _set_key(self, str):
+ if str == "*":
+ self.applied = False
+ self.delegate = None
+ return
+
+ applied = False
+ try:
+ self.delegate = User.objects.get(id = str)
+ self.applied = True
+ except:
+ pass
+
+ def kwargs(self):
+ if not self.applied:
+ return {}
+ return {'delegate': self.delegate}
+
+ def condition(self):
+ if self.delegate:
+ return self.delegate.get_profile().name()
+ return 'Nobody'
+
+ def _form(self):
+ delegates = User.objects.filter(userprofile__maintainer_projects =
+ self.filters.project)
+
+ str = '<select name="delegate">'
+
+ selected = ''
+ if not self.applied:
+ selected = 'selected'
+
+ str += '<option %s value="*">------</option>' % selected
+
+ selected = ''
+ if self.delegate is None:
+ selected = 'selected'
+
+ str += '<option %s value="">Nobody</option>' % selected
+
+ for d in delegates:
+ selected = ''
+ if d == self.delegate:
+ selected = ' selected'
+
+ str += '<option %s value="%s">%s</option>' % (selected,
+ d.id, d.get_profile().name())
+ str += '</select>'
+
+ return mark_safe(str)
+
+ def key(self):
+ if self.delegate:
+ return self.delegate.id
+ if self.applied:
+ return None
+ return '*'
+
+ def url_without_me(self):
+ qs = self.filters.querystring_without_filter(self)
+ if qs != '?':
+ qs += '&'
+ return qs + ('%s=*' % self.param)
+
+ def set_status(self, *args, **kwargs):
+ if 'delegate' in kwargs:
+ self.applied = self.forced = True
+ self.delegate = kwargs['delegate']
+ if self.AnyDelegate in args:
+ self.applied = False
+ self.forced = True
+
+filterclasses = [SubmitterFilter, \
+ StateFilter,
+ SearchFilter,
+ ArchiveFilter,
+ DelegateFilter]
+
+class Filters:
+
+ def __init__(self, request):
+ self._filters = map(lambda c: c(self), filterclasses)
+ self.dict = request.GET
+ self.project = None
+
+ for f in self._filters:
+ f.parse(self.dict)
+
+ def set_project(self, project):
+ self.project = project
+
+ def filter_conditions(self):
+ kwargs = {}
+ for f in self._filters:
+ if f.applied:
+ kwargs.update(f.kwargs())
+ return kwargs
+
+ def apply(self, queryset):
+ kwargs = self.filter_conditions()
+ if not kwargs:
+ return queryset
+ return queryset.filter(**kwargs)
+
+ def params(self):
+ return [ (f.param, f.key()) for f in self._filters \
+ if f.key() is not None ]
+
+ def querystring(self, remove = None):
+ params = dict(self.params())
+
+ for (k, v) in self.dict.iteritems():
+ if k not in params:
+ params[k] = v[0]
+
+ if remove is not None:
+ if remove.param in params.keys():
+ del params[remove.param]
+
+ return '?' + '&'.join(['%s=%s' % x for x in params.iteritems()])
+
+ def querystring_without_filter(self, filter):
+ return self.querystring(filter)
+
+ def applied_filters(self):
+ return filter(lambda x: x.applied, self._filters)
+
+ def available_filters(self):
+ return self._filters
+
+ def set_status(self, filterclass, *args, **kwargs):
+ for f in self._filters:
+ if isinstance(f, filterclass):
+ f.set_status(*args, **kwargs)
+ return
diff --git a/apps/patchwork/forms.py b/apps/patchwork/forms.py
new file mode 100644
index 0000000..ed55c4f
--- /dev/null
+++ b/apps/patchwork/forms.py
@@ -0,0 +1,213 @@
+# 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
+
+
+from django.contrib.auth.models import User
+from django import newforms as forms
+
+from patchwork.models import RegistrationRequest, Patch, State, Bundle, \
+ UserProfile
+
+class RegisterForm(forms.ModelForm):
+ password = forms.CharField(widget = forms.PasswordInput)
+ email = forms.EmailField(max_length = 200)
+
+ class Meta:
+ model = RegistrationRequest
+ exclude = ['key']
+
+ def clean_email(self):
+ value = self.cleaned_data['email']
+ try:
+ User.objects.get(email = value)
+ raise forms.ValidationError(('The email address %s has ' +
+ 'has already been registered') % value)
+ except User.DoesNotExist:
+ pass
+ try:
+ RegistrationRequest.objects.get(email = value)
+ raise forms.ValidationError(('The email address %s has ' +
+ 'has already been registered') % value)
+ except RegistrationRequest.DoesNotExist:
+ pass
+ return value
+
+ def clean_username(self):
+ value = self.cleaned_data['username']
+ try:
+ User.objects.get(username = value)
+ raise forms.ValidationError(('The username %s has ' +
+ 'has already been registered') % value)
+ except User.DoesNotExist:
+ pass
+ try:
+ RegistrationRequest.objects.get(username = value)
+ raise forms.ValidationError(('The username %s has ' +
+ 'has already been registered') % value)
+ except RegistrationRequest.DoesNotExist:
+ pass
+ return value
+
+class LoginForm(forms.Form):
+ username = forms.CharField(max_length = 30)
+ password = forms.CharField(widget = forms.PasswordInput)
+
+class BundleForm(forms.ModelForm):
+ class Meta:
+ model = Bundle
+ fields = ['name', 'public']
+
+class CreateBundleForm(forms.ModelForm):
+ def __init__(self, *args, **kwargs):
+ super(CreateBundleForm, self).__init__(*args, **kwargs)
+
+ class Meta:
+ model = Bundle
+ fields = ['name']
+
+ def clean_name(self):
+ name = self.cleaned_data['name']
+ count = Bundle.objects.filter(owner = self.instance.owner, \
+ name = name).count()
+ if count > 0:
+ raise forms.ValidationError('A bundle called %s already exists' \
+ % name)
+ return name
+
+class DelegateField(forms.ModelChoiceField):
+ def __init__(self, project, *args, **kwargs):
+ queryset = User.objects.filter(userprofile__in = \
+ UserProfile.objects \
+ .filter(maintainer_projects = project) \
+ .values('pk').query)
+ super(DelegateField, self).__init__(queryset, *args, **kwargs)
+
+
+class PatchForm(forms.ModelForm):
+ def __init__(self, instance = None, project = None, *args, **kwargs):
+ if (not project) and instance:
+ project = instance.project
+ if not project:
+ raise Exception("meep")
+ super(PatchForm, self).__init__(instance = instance, *args, **kwargs)
+ self.fields['delegate'] = DelegateField(project)
+
+ class Meta:
+ model = Patch
+ fields = ['state', 'archived', 'delegate']
+
+class UserProfileForm(forms.ModelForm):
+ class Meta:
+ model = UserProfile
+ fields = ['primary_project', 'patches_per_page']
+
+class OptionalDelegateField(DelegateField):
+ no_change_choice = ('*', 'no change')
+
+ def __init__(self, no_change_choice = None, *args, **kwargs):
+ self.filter = None
+ if (no_change_choice):
+ self.no_change_choice = no_change_choice
+ super(OptionalDelegateField, self). \
+ __init__(initial = self.no_change_choice[0], *args, **kwargs)
+
+ def _get_choices(self):
+ choices = list(
+ super(OptionalDelegateField, self)._get_choices())
+ choices.append(self.no_change_choice)
+ return choices
+
+ choices = property(_get_choices, forms.ChoiceField._set_choices)
+
+ def is_no_change(self, value):
+ return value == self.no_change_choice[0]
+
+ def clean(self, value):
+ if value == self.no_change_choice[0]:
+ return value
+ return super(OptionalDelegateField, self).clean(value)
+
+class OptionalModelChoiceField(forms.ModelChoiceField):
+ no_change_choice = ('*', 'no change')
+
+ def __init__(self, no_change_choice = None, *args, **kwargs):
+ self.filter = None
+ if (no_change_choice):
+ self.no_change_choice = no_change_choice
+ super(OptionalModelChoiceField, self). \
+ __init__(initial = self.no_change_choice[0], *args, **kwargs)
+
+ def _get_choices(self):
+ choices = list(
+ super(OptionalModelChoiceField, self)._get_choices())
+ choices.append(self.no_change_choice)
+ return choices
+
+ choices = property(_get_choices, forms.ChoiceField._set_choices)
+
+ def is_no_change(self, value):
+ return value == self.no_change_choice[0]
+
+ def clean(self, value):
+ if value == self.no_change_choice[0]:
+ return value
+ return super(OptionalModelChoiceField, self).clean(value)
+
+class MultipleBooleanField(forms.ChoiceField):
+ no_change_choice = ('*', 'no change')
+ def __init__(self, *args, **kwargs):
+ super(MultipleBooleanField, self).__init__(*args, **kwargs)
+ self.choices = [self.no_change_choice] + \
+ [(True, 'Archived'), (False, 'Unarchived')]
+
+ def is_no_change(self, value):
+ return value == self.no_change_choice[0]
+
+class MultiplePatchForm(PatchForm):
+ state = OptionalModelChoiceField(queryset = State.objects.all())
+ archived = MultipleBooleanField()
+
+ def __init__(self, project, *args, **kwargs):
+ super(MultiplePatchForm, self).__init__(project = project,
+ *args, **kwargs)
+ self.fields['delegate'] = OptionalDelegateField(project = project)
+
+ def save(self, instance, commit = True):
+ opts = instance.__class__._meta
+ if self.errors:
+ raise ValueError("The %s could not be changed because the data "
+ "didn't validate." % opts.object_name)
+ data = self.cleaned_data
+ # remove 'no change fields' from the data
+ for f in opts.fields:
+ if not f.name in data:
+ continue
+
+ field = getattr(self, f.name, None)
+ if not field:
+ continue
+
+ if field.is_no_change(data[f.name]):
+ del data[f.name]
+
+ return forms.save_instance(self, instance,
+ self._meta.fields, 'changed', commit)
+
+class UserPersonLinkForm(forms.Form):
+ email = forms.EmailField(max_length = 200)
diff --git a/apps/patchwork/models.py b/apps/patchwork/models.py
new file mode 100644
index 0000000..f6943fc
--- /dev/null
+++ b/apps/patchwork/models.py
@@ -0,0 +1,362 @@
+# 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
+
+from django.db import models
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.contrib.sites.models import Site
+from django.conf import settings
+import django.oldforms as oldforms
+
+import re
+import datetime, time
+import string
+import random
+import hashlib
+from email.mime.text import MIMEText
+import email.utils
+
+class Person(models.Model):
+ email = models.CharField(max_length=255, unique = True)
+ name = models.CharField(max_length=255, null = True)
+ user = models.ForeignKey(User, null = True)
+
+ def __str__(self):
+ if self.name:
+ return '%s <%s>' % (self.name, self.email)
+ else:
+ return self.email
+
+ def link_to_user(self, user):
+ self.name = user.get_profile().name()
+ self.user = user
+
+ class Meta:
+ verbose_name_plural = 'People'
+
+ class Admin:
+ pass
+
+class Project(models.Model):
+ linkname = models.CharField(max_length=255, unique=True)
+ name = models.CharField(max_length=255, unique=True)
+ listid = models.CharField(max_length=255, unique=True)
+ listemail = models.CharField(max_length=200)
+
+ def __str__(self):
+ return self.name
+
+ class Admin:
+ pass
+
+class UserProfile(models.Model):
+ user = models.ForeignKey(User, unique = True)
+ primary_project = models.ForeignKey(Project, null = True)
+ maintainer_projects = models.ManyToManyField(Project,
+ related_name = 'maintainer_project')
+ send_email = models.BooleanField(default = False,
+ help_text = 'Selecting this option allows patchwork to send ' +
+ 'email on your behalf')
+ patches_per_page = models.PositiveIntegerField(default = 100,
+ null = False, blank = False,
+ help_text = 'Number of patches to display per page')
+
+ def name(self):
+ if self.user.first_name or self.user.last_name:
+ names = filter(bool, [self.user.first_name, self.user.last_name])
+ return ' '.join(names)
+ return self.user.username
+
+ def contributor_projects(self):
+ submitters = Person.objects.filter(user = self.user)
+ return Project.objects \
+ .filter(id__in = \
+ Patch.objects.filter(
+ submitter__in = submitters) \
+ .values('project_id').query)
+
+
+ def sync_person(self):
+ pass
+
+ def n_todo_patches(self):
+ return self.todo_patches().count()
+
+ def todo_patches(self, project = None):
+
+ # filter on project, if necessary
+ if project:
+ qs = Patch.objects.filter(project = project)
+ else:
+ qs = Patch.objects
+
+ qs = qs.filter(archived = False) \
+ .filter(delegate = self.user) \
+ .filter(state__in = \
+ State.objects.filter(action_required = True) \
+ .values('pk').query)
+ return qs
+
+ def save(self):
+ super(UserProfile, self).save()
+ people = Person.objects.filter(email = self.user.email)
+ if not people:
+ person = Person(email = self.user.email,
+ name = self.name(), user = self.user)
+ person.save()
+ else:
+ for person in people:
+ person.user = self.user
+ person.save()
+
+ class Admin:
+ pass
+
+ def __str__(self):
+ return self.name()
+
+def _confirm_key():
+ allowedchars = string.ascii_lowercase + string.digits
+ str = ''
+ for i in range(1, 32):
+ str += random.choice(allowedchars)
+ return str;
+
+class RegistrationRequest(models.Model):
+ username = models.CharField(max_length = 30, unique = True)
+ first_name = models.CharField(max_length = 50)
+ last_name = models.CharField(max_length = 50)
+ email = models.CharField(max_length = 200, unique = True)
+ password = models.CharField(max_length = 200)
+ key = models.CharField(max_length = 32, default = _confirm_key)
+
+ def create_user(self):
+ user = User.objects.create_user(self.username,
+ self.email, self.password)
+ user.first_name = self.first_name
+ user.last_name = self.last_name
+ user.save()
+ profile = UserProfile(user = user)
+ profile.save()
+ self.delete()
+
+ # link a person to this user. if none exists, create.
+ person = None
+ try:
+ person = Person.objects.get(email = user.email)
+ except Exception:
+ pass
+ if not person:
+ person = Person(email = user.email)
+
+ person.link_to_user(user)
+ person.save()
+
+ return user
+
+ class Admin:
+ pass
+
+class UserPersonConfirmation(models.Model):
+ user = models.ForeignKey(User)
+ email = models.CharField(max_length = 200)
+ date = models.DateTimeField(default=datetime.datetime.now)
+ key = models.CharField(max_length = 32, default = _confirm_key)
+
+ def confirm(self):
+ person = None
+ try:
+ person = Person.objects.get(email = self.email)
+ except Exception:
+ pass
+ if not person:
+ person = Person(email = self.email)
+
+ person.link_to_user(self.user)
+ person.save()
+
+
+ class Admin:
+ pass
+
+
+class State(models.Model):
+ name = models.CharField(max_length = 100)
+ ordering = models.IntegerField(unique = True)
+ action_required = models.BooleanField(default = True)
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ ordering = ['ordering']
+
+ class Admin:
+ pass
+
+class HashField(models.Field):
+ __metaclass__ = models.SubfieldBase
+
+ def __init__(self, algorithm = 'sha1', *args, **kwargs):
+ self.algorithm = algorithm
+ super(HashField, self).__init__(*args, **kwargs)
+
+ def db_type(self):
+ n_bytes = len(hashlib.new(self.algorithm).digest())
+ if settings.DATABASE_ENGINE == 'postgresql':
+ return 'bytea'
+ elif settings.DATABASE_ENGINE == 'mysql':
+ return 'binary(%d)' % n_bytes
+
+ def to_python(self, value):
+ return value
+
+ def get_db_prep_save(self, value):
+ return ''.join(map(lambda x: '\\%03o' % ord(x), value))
+
+ def get_manipulator_field_objs(self):
+ return [oldforms.TextField]
+
+class Patch(models.Model):
+ project = models.ForeignKey(Project)
+ msgid = models.CharField(max_length=255, unique = True)
+ name = models.CharField(max_length=255)
+ date = models.DateTimeField(default=datetime.datetime.now)
+ submitter = models.ForeignKey(Person)
+ delegate = models.ForeignKey(User, blank = True, null = True)
+ state = models.ForeignKey(State)
+ archived = models.BooleanField(default = False)
+ headers = models.TextField(blank = True)
+ content = models.TextField()
+ commit_ref = models.CharField(max_length=255, null = True, blank = True)
+ hash = HashField()
+
+ def __str__(self):
+ return self.name
+
+ def comments(self):
+ return Comment.objects.filter(patch = self)
+
+ def save(self):
+ try:
+ s = self.state
+ except:
+ self.state = State.objects.get(ordering = 0)
+ if hash is None:
+ print "no hash"
+ super(Patch, self).save()
+
+ def is_editable(self, user):
+ if not user.is_authenticated():
+ return False
+
+ if self.submitter.user == user or self.delegate == user:
+ return True
+
+ profile = user.get_profile()
+ return self.project in user.get_profile().maintainer_projects.all()
+
+ def form(self):
+ f = PatchForm(instance = self, prefix = self.id)
+ return f
+
+ def filename(self):
+ fname_re = re.compile('[^-_A-Za-z0-9\.]+')
+ str = fname_re.sub('-', self.name)
+ return str.strip('-') + '.patch'
+
+ def mbox(self):
+ comment = None
+ try:
+ comment = Comment.objects.get(msgid = self.msgid)
+ except Exception:
+ pass
+
+ body = ''
+ if comment:
+ body = comment.content.strip() + "\n\n"
+ body += self.content
+
+ mail = MIMEText(body)
+ mail['Subject'] = self.name
+ mail['Date'] = email.utils.formatdate(
+ time.mktime(self.date.utctimetuple()))
+ mail['From'] = str(self.submitter)
+ mail['X-Patchwork-Id'] = str(self.id)
+ mail.set_unixfrom('From patchwork ' + self.date.ctime())
+
+ return mail
+
+
+ @models.permalink
+ def get_absolute_url(self):
+ return ('patchwork.views.patch.patch', (), {'patch_id': self.id})
+
+ class Meta:
+ verbose_name_plural = 'Patches'
+ ordering = ['date']
+
+ class Admin:
+ pass
+
+class Comment(models.Model):
+ patch = models.ForeignKey(Patch)
+ msgid = models.CharField(max_length=255, unique = True)
+ submitter = models.ForeignKey(Person)
+ date = models.DateTimeField(default = datetime.datetime.now)
+ headers = models.TextField(blank = True)
+ content = models.TextField()
+
+ class Admin:
+ pass
+
+ class Meta:
+ ordering = ['date']
+
+class Bundle(models.Model):
+ owner = models.ForeignKey(User)
+ project = models.ForeignKey(Project)
+ name = models.CharField(max_length = 50, null = False, blank = False)
+ patches = models.ManyToManyField(Patch)
+ public = models.BooleanField(default = False)
+
+ def n_patches(self):
+ return self.patches.all().count()
+
+ class Meta:
+ unique_together = [('owner', 'name')]
+
+ class Admin:
+ pass
+
+ def public_url(self):
+ if not self.public:
+ return None
+ site = Site.objects.get_current()
+ return 'http://%s%s' % (site.domain,
+ reverse('patchwork.views.bundle.public',
+ kwargs = {
+ 'username': self.owner.username,
+ 'bundlename': self.name
+ }))
+
+ def mbox(self):
+ return '\n'.join([p.mbox().as_string(True) \
+ for p in self.patches.all()])
+
diff --git a/apps/patchwork/paginator.py b/apps/patchwork/paginator.py
new file mode 100644
index 0000000..8d8be64
--- /dev/null
+++ b/apps/patchwork/paginator.py
@@ -0,0 +1,88 @@
+# 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
+
+
+from django.core import paginator
+from django.conf import settings
+
+DEFAULT_PATCHES_PER_PAGE = 100
+LONG_PAGE_THRESHOLD = 30
+LEADING_PAGE_RANGE_DISPLAYED = TRAILING_PAGE_RANGE_DISPLAYED = 10
+LEADING_PAGE_RANGE = TRAILING_PAGE_RANGE = 8
+NUM_PAGES_OUTSIDE_RANGE = 2
+ADJACENT_PAGES = 4
+
+# parts from:
+# http://blog.localkinegrinds.com/2007/09/06/digg-style-pagination-in-django/
+
+class Paginator(paginator.Paginator):
+ def __init__(self, request, objects):
+
+ patches_per_page = settings.DEFAULT_PATCHES_PER_PAGE
+
+ if request.user.is_authenticated():
+ patches_per_page = request.user.get_profile().patches_per_page
+
+ n = request.META.get('ppp')
+ if n:
+ try:
+ patches_per_page = int(n)
+ except ValueError:
+ pass
+
+ super(Paginator, self).__init__(objects, patches_per_page)
+
+ try:
+ page_no = int(request.GET.get('page'))
+ self.current_page = self.page(int(page_no))
+ except Exception:
+ page_no = 1
+ self.current_page = self.page(page_no)
+
+ self.leading_set = self.trailing_set = []
+
+ pages = self.num_pages
+
+ if pages <= LEADING_PAGE_RANGE_DISPLAYED:
+ self.adjacent_set = [n for n in range(1, pages + 1) \
+ if n > 0 and n <= pages]
+ elif page_no <= LEADING_PAGE_RANGE:
+ self.adjacent_set = [n for n in \
+ range(1, LEADING_PAGE_RANGE_DISPLAYED + 1) \
+ if n > 0 and n <= pages]
+ self.leading_set = [n + pages for n in \
+ range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ elif page_no > pages - TRAILING_PAGE_RANGE:
+ self.adjacent_set = [n for n in \
+ range(pages - TRAILING_PAGE_RANGE_DISPLAYED + 1, \
+ pages + 1) if n > 0 and n <= pages]
+ self.trailing_set = [n + 1 for n in range(0, \
+ NUM_PAGES_OUTSIDE_RANGE)]
+ else:
+ self.adjacent_set = [n for n in range(page_no - ADJACENT_PAGES, \
+ page_no + ADJACENT_PAGES + 1) if n > 0 and n <= pages]
+ self.leading_set = [n + pages for n in \
+ range(0, -NUM_PAGES_OUTSIDE_RANGE, -1)]
+ self.trailing_set = [n + 1 for n in \
+ range(0, NUM_PAGES_OUTSIDE_RANGE)]
+
+
+ self.leading_set.reverse()
+ self.long_page = \
+ len(self.current_page.object_list) >= LONG_PAGE_THRESHOLD
diff --git a/apps/patchwork/parser.py b/apps/patchwork/parser.py
new file mode 100644
index 0000000..ecc1d4b
--- /dev/null
+++ b/apps/patchwork/parser.py
@@ -0,0 +1,206 @@
+#!/usr/bin/python
+#
+# 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
+
+
+import re
+import hashlib
+
+_hunk_re = re.compile('^\@\@ -\d+(?:,(\d+))? \+\d+(?:,(\d+))? \@\@')
+_filename_re = re.compile('^(---|\+\+\+) (\S+)')
+
+def parse_patch(text):
+ patchbuf = ''
+ commentbuf = ''
+ buf = ''
+
+ # state specified the line we just saw, and what to expect next
+ state = 0
+ # 0: text
+ # 1: suspected patch header (diff, ====, Index:)
+ # 2: patch header line 1 (---)
+ # 3: patch header line 2 (+++)
+ # 4: patch hunk header line (@@ line)
+ # 5: patch hunk content
+ #
+ # valid transitions:
+ # 0 -> 1 (diff, ===, Index:)
+ # 0 -> 2 (---)
+ # 1 -> 2 (---)
+ # 2 -> 3 (+++)
+ # 3 -> 4 (@@ line)
+ # 4 -> 5 (patch content)
+ # 5 -> 1 (run out of lines from @@-specifed count)
+ #
+ # Suspected patch header is stored into buf, and appended to
+ # patchbuf if we find a following hunk. Otherwise, append to
+ # comment after parsing.
+
+ # line counts while parsing a patch hunk
+ lc = (0, 0)
+ hunk = 0
+
+
+ for line in text.split('\n'):
+ line += '\n'
+
+ if state == 0:
+ if line.startswith('diff') or line.startswith('===') \
+ or line.startswith('Index: '):
+ state = 1
+ buf += line
+
+ elif line.startswith('--- '):
+ state = 2
+ buf += line
+
+ else:
+ commentbuf += line
+
+ elif state == 1:
+ buf += line
+ if line.startswith('--- '):
+ state = 2
+
+ elif state == 2:
+ if line.startswith('+++ '):
+ state = 3
+ buf += line
+
+ elif hunk:
+ state = 1
+ buf += line
+
+ else:
+ state = 0
+ commentbuf += buf + line
+ buf = ''
+
+ elif state == 3:
+ match = _hunk_re.match(line)
+ if match:
+
+ def fn(x):
+ if not x:
+ return 1
+ return int(x)
+
+ lc = map(fn, match.groups())
+
+ state = 4
+ patchbuf += buf + line
+ buf = ''
+
+ elif line.startswith('--- '):
+ patchbuf += buf + line
+ buf = ''
+ state = 2
+
+ elif hunk:
+ state = 1
+ buf += line
+
+ else:
+ state = 0
+ commentbuf += buf + line
+ buf = ''
+
+ elif state == 4 or state == 5:
+ if line.startswith('-'):
+ lc[0] -= 1
+ elif line.startswith('+'):
+ lc[1] -= 1
+ else:
+ lc[0] -= 1
+ lc[1] -= 1
+
+ patchbuf += line
+
+ if lc[0] <= 0 and lc[1] <= 0:
+ state = 3
+ hunk += 1
+ else:
+ state = 5
+
+ else:
+ raise Exception("Unknown state %d! (line '%s')" % (state, line))
+
+ commentbuf += buf
+
+ if patchbuf == '':
+ patchbuf = None
+
+ if commentbuf == '':
+ commentbuf = None
+
+ return (patchbuf, commentbuf)
+
+def patch_hash(str):
+ str = str.replace('\r', '')
+ str = str.strip() + '\n'
+ lines = str.split('\n')
+
+ prefixes = ['-', '+', ' ']
+ hash = hashlib.sha1()
+
+ for line in str.split('\n'):
+
+ if len(line) <= 0:
+ continue
+
+ hunk_match = _hunk_re.match(line)
+ filename_match = _filename_re.match(line)
+
+ if filename_match:
+ # normalise -p1 top-directories
+ if filename_match.group(1) == '---':
+ filename = 'a/'
+ else:
+ filename = 'b/'
+ filename += '/'.join(filename_match.group(2).split('/')[1:])
+
+ line = filename_match.group(1) + ' ' + filename
+
+
+ elif hunk_match:
+ # remove line numbers
+ def fn(x):
+ if not x:
+ return 1
+ return int(x)
+ line_nos = map(fn, hunk_match.groups())
+ line = '@@ -%d +%d @@' % tuple(line_nos)
+
+ elif line[0] in prefixes:
+ pass
+
+ else:
+ continue
+
+ hash.update(line + '\n')
+
+if __name__ == '__main__':
+ import sys
+# (patch, comment) = parse_patch(sys.stdin.read())
+# if patch:
+# print "Patch: ------\n" + patch
+# if comment:
+# print "Comment: ----\n" + comment
+ normalise_patch_content(sys.stdin.read())
diff --git a/apps/patchwork/requestcontext.py b/apps/patchwork/requestcontext.py
new file mode 100644
index 0000000..cb9a782
--- /dev/null
+++ b/apps/patchwork/requestcontext.py
@@ -0,0 +1,82 @@
+# 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
+
+from django.template import RequestContext
+from django.utils.html import escape
+from patchwork.filters import Filters
+from patchwork.models import Bundle, Project
+
+def bundle(request):
+ user = request.user
+ if not user.is_authenticated():
+ return {}
+ return {'bundles': Bundle.objects.filter(owner = user)}
+
+def _params_as_qs(params):
+ return '&'.join([ '%s=%s' % (escape(k), escape(v)) for k, v in params ])
+
+def _params_as_hidden_fields(params):
+ return '\n'.join([ '<input type="hidden" name="%s" value="%s"/>' % \
+ (escape(k), escape(v)) for k, v in params ])
+
+class PatchworkRequestContext(RequestContext):
+ def __init__(self, request, project = None,
+ dict = None, processors = None,
+ list_view = None, list_view_params = {}):
+ self._project = project
+ self.filters = Filters(request)
+ if processors is None:
+ processors = []
+ processors.append(bundle)
+ super(PatchworkRequestContext, self). \
+ __init__(request, dict, processors);
+
+ self.update({'filters': self.filters})
+ if list_view:
+ params = self.filters.params()
+ for param in ['order', 'page']:
+ value = request.REQUEST.get(param, None)
+ if value:
+ params.append((param, value))
+ self.update({
+ 'list_view': {
+ 'view': list_view,
+ 'view_params': list_view_params,
+ 'params': params
+ }})
+
+ self.projects = Project.objects.all()
+
+ self.update({
+ 'project': self.project,
+ 'other_projects': len(self.projects) > 1
+ })
+
+ def _set_project(self, project):
+ self._project = project
+ self.filters.set_project(project)
+ self.update({'project': self._project})
+
+ def _get_project(self):
+ return self._project
+
+ project = property(_get_project, _set_project)
+
+ def add_message(self, message):
+ self['messages'].append(message)
diff --git a/apps/patchwork/sql/project.sql b/apps/patchwork/sql/project.sql
new file mode 100644
index 0000000..f0db525
--- /dev/null
+++ b/apps/patchwork/sql/project.sql
@@ -0,0 +1,6 @@
+insert into patchwork_project (linkname, name, listid, listemail)
+ values ('cbe-oss-dev', 'Cell Broadband Engine development',
+ 'cbe-oss-dev.ozlabs.org', 'cbe-oss-dev@ozlabs.org');
+insert into patchwork_project (linkname, name, listid, listemail)
+ values ('linuxppc-dev', 'Linux PPC development',
+ 'linuxppc-dev.ozlabs.org', 'linuxppc-dev@ozlabs.org');
diff --git a/apps/patchwork/sql/state.sql b/apps/patchwork/sql/state.sql
new file mode 100644
index 0000000..c673fd8
--- /dev/null
+++ b/apps/patchwork/sql/state.sql
@@ -0,0 +1,20 @@
+insert into patchwork_state (ordering, name, action_required) values
+ (0, 'New', True);
+insert into patchwork_state (ordering, name, action_required) values
+ (1, 'Under Review', True);
+insert into patchwork_state (ordering, name, action_required) values
+ (2, 'Accepted', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (3, 'Rejected', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (4, 'RFC', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (5, 'Not Applicable', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (6, 'Changes Requested', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (7, 'Awaiting Upstream', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (8, 'Superseded', False);
+insert into patchwork_state (ordering, name, action_required) values
+ (9, 'Deferred', False);
diff --git a/apps/patchwork/templatetags/__init__.py b/apps/patchwork/templatetags/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/apps/patchwork/templatetags/__init__.py
diff --git a/apps/patchwork/templatetags/filter.py b/apps/patchwork/templatetags/filter.py
new file mode 100644
index 0000000..b940599
--- /dev/null
+++ b/apps/patchwork/templatetags/filter.py
@@ -0,0 +1,36 @@
+# 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
+
+from django import template
+from django.utils.html import escape
+
+import re
+
+
+register = template.Library()
+
+@register.filter
+def personify(person):
+ if person.name:
+ linktext = escape(person.name)
+ else:
+ linktext = escape(person.email)
+
+ return '<a href="javascript:personpopup(\'%s\')">%s</a>' % (escape(person.email), linktext)
+
diff --git a/apps/patchwork/templatetags/listurl.py b/apps/patchwork/templatetags/listurl.py
new file mode 100644
index 0000000..22e2a1b
--- /dev/null
+++ b/apps/patchwork/templatetags/listurl.py
@@ -0,0 +1,136 @@
+# 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
+
+from django import template
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.encoding import smart_str
+from patchwork.filters import filterclasses
+from django.conf import settings
+from django.core.urlresolvers import reverse, NoReverseMatch
+import re
+
+register = template.Library()
+
+# params to preserve across views
+list_params = [ c.param for c in filterclasses ] + ['order', 'page']
+
+class ListURLNode(template.defaulttags.URLNode):
+ def __init__(self, kwargs):
+ super(ListURLNode, self).__init__(None, [], {})
+ self.params = {}
+ for (k, v) in kwargs.iteritems():
+ if k in list_params:
+ self.params[k] = v
+
+ def render(self, context):
+ view_name = template.Variable('list_view.view').resolve(context)
+ kwargs = template.Variable('list_view.view_params') \
+ .resolve(context)
+
+ str = None
+ try:
+ str = reverse(view_name, args=[], kwargs=kwargs)
+ except NoReverseMatch:
+ try:
+ project_name = settings.SETTINGS_MODULE.split('.')[0]
+ str = reverse(project_name + '.' + view_name,
+ args=[], kwargs=kwargs)
+ except NoReverseMatch:
+ raise
+
+ if str is None:
+ return ''
+
+ params = []
+ try:
+ qs_var = template.Variable('list_view.params')
+ params = dict(qs_var.resolve(context))
+ except Exception:
+ pass
+
+ for (k, v) in self.params.iteritems():
+ params[smart_str(k,'ascii')] = v.resolve(context)
+
+ if not params:
+ return str
+
+ return str + '?' + '&'.join(['%s=%s' % (k, escape(v)) \
+ for (k, v) in params.iteritems()])
+
+@register.tag
+def listurl(parser, token):
+ bits = token.contents.split(' ', 1)
+ if len(bits) < 1:
+ raise TemplateSyntaxError("'%s' takes at least one argument"
+ " (path to a view)" % bits[0])
+ kwargs = {}
+ if len(bits) > 1:
+ for arg in bits[1].split(','):
+ if '=' in arg:
+ k, v = arg.split('=', 1)
+ k = k.strip()
+ kwargs[k] = parser.compile_filter(v)
+ else:
+ raise TemplateSyntaxError("'%s' requires name=value params" \
+ % bits[0])
+ return ListURLNode(kwargs)
+
+class ListFieldsNode(template.Node):
+ def __init__(self, params):
+ self.params = params
+
+ def render(self, context):
+ self.view_name = template.Variable('list_view.view').resolve(context)
+ try:
+ qs_var = template.Variable('list_view.params')
+ params = dict(qs_var.resolve(context))
+ except Exception:
+ pass
+
+ params.update(self.params)
+
+ if not params:
+ return ''
+
+ str = ''
+ for (k, v) in params.iteritems():
+ str += '<input type="hidden" name="%s" value="%s"\>' % \
+ (k, escape(v))
+
+ return mark_safe(str)
+
+@register.tag
+def listfields(parser, token):
+ bits = token.contents.split(' ', 1)
+ if len(bits) < 1:
+ raise TemplateSyntaxError("'%s' takes at least one argument"
+ " (path to a view)" % bits[0])
+ params = {}
+ if len(bits) > 2:
+ for arg in bits[2].split(','):
+ if '=' in arg:
+ k, v = arg.split('=', 1)
+ k = k.strip()
+ params[k] = parser.compile_filter(v)
+ else:
+ raise TemplateSyntaxError("'%s' requires name=value params" \
+ % bits[0])
+ return ListFieldsNode(bits[1], params)
+
diff --git a/apps/patchwork/templatetags/order.py b/apps/patchwork/templatetags/order.py
new file mode 100644
index 0000000..e392f03
--- /dev/null
+++ b/apps/patchwork/templatetags/order.py
@@ -0,0 +1,66 @@
+# 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
+
+
+from django import template
+import re
+
+register = template.Library()
+
+@register.tag(name = 'ifpatcheditable')
+def do_patch_is_editable(parser, token):
+ try:
+ tag_name, name, cur_order = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError("%r tag requires two arguments" \
+ % token.contents.split()[0])
+
+ end_tag = 'endifpatcheditable'
+ nodelist_true = parser.parse([end_tag, 'else'])
+
+ token = parser.next_token()
+ if token.contents == 'else':
+ nodelist_false = parser.parse([end_tag])
+ parser.delete_first_token()
+ else:
+ nodelist_false = template.NodeList()
+
+ return EditablePatchNode(patch_var, nodelist_true, nodelist_false)
+
+class EditablePatchNode(template.Node):
+ def __init__(self, patch_var, nodelist_true, nodelist_false):
+ self.nodelist_true = nodelist_true
+ self.nodelist_false = nodelist_false
+ self.patch_var = template.Variable(patch_var)
+ self.user_var = template.Variable('user')
+
+ def render(self, context):
+ try:
+ patch = self.patch_var.resolve(context)
+ user = self.user_var.resolve(context)
+ except template.VariableDoesNotExist:
+ return ''
+
+ if not user.is_authenticated():
+ return self.nodelist_false.render(context)
+
+ if not patch.is_editable(user):
+ return self.nodelist_false.render(context)
+
+ return self.nodelist_true.render(context)
diff --git a/apps/patchwork/templatetags/patch.py b/apps/patchwork/templatetags/patch.py
new file mode 100644
index 0000000..bec0cab
--- /dev/null
+++ b/apps/patchwork/templatetags/patch.py
@@ -0,0 +1,65 @@
+# 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
+
+from django import template
+import re
+
+register = template.Library()
+
+@register.tag(name = 'ifpatcheditable')
+def do_patch_is_editable(parser, token):
+ try:
+ tag_name, patch_var = token.split_contents()
+ except ValueError:
+ raise template.TemplateSyntaxError("%r tag requires one argument" \
+ % token.contents.split()[0])
+
+ end_tag = 'endifpatcheditable'
+ nodelist_true = parser.parse([end_tag, 'else'])
+
+ token = parser.next_token()
+ if token.contents == 'else':
+ nodelist_false = parser.parse([end_tag])
+ parser.delete_first_token()
+ else:
+ nodelist_false = template.NodeList()
+
+ return EditablePatchNode(patch_var, nodelist_true, nodelist_false)
+
+class EditablePatchNode(template.Node):
+ def __init__(self, patch_var, nodelist_true, nodelist_false):
+ self.nodelist_true = nodelist_true
+ self.nodelist_false = nodelist_false
+ self.patch_var = template.Variable(patch_var)
+ self.user_var = template.Variable('user')
+
+ def render(self, context):
+ try:
+ patch = self.patch_var.resolve(context)
+ user = self.user_var.resolve(context)
+ except template.VariableDoesNotExist:
+ return ''
+
+ if not user.is_authenticated():
+ return self.nodelist_false.render(context)
+
+ if not patch.is_editable(user):
+ return self.nodelist_false.render(context)
+
+ return self.nodelist_true.render(context)
diff --git a/apps/patchwork/templatetags/person.py b/apps/patchwork/templatetags/person.py
new file mode 100644
index 0000000..6a6a6af
--- /dev/null
+++ b/apps/patchwork/templatetags/person.py
@@ -0,0 +1,40 @@
+# 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
+
+from django import template
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+import re
+
+register = template.Library()
+
+@register.filter
+def personify(person):
+
+ if person.name:
+ linktext = escape(person.name)
+ else:
+ linktext = escape(person.email)
+
+ str = '<a href="mailto:%s">%s</a>' % \
+ (escape(person.email), linktext)
+
+ return mark_safe(str)
+
+
diff --git a/apps/patchwork/templatetags/pwurl.py b/apps/patchwork/templatetags/pwurl.py
new file mode 100644
index 0000000..98bc1ca
--- /dev/null
+++ b/apps/patchwork/templatetags/pwurl.py
@@ -0,0 +1,76 @@
+# 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
+
+from django import template
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from patchwork.filters import filterclasses
+import re
+
+register = template.Library()
+
+# params to preserve across views
+list_params = [ c.param for c in filterclasses ] + ['order', 'page']
+
+class ListURLNode(template.defaulttags.URLNode):
+ def __init__(self, *args, **kwargs):
+ super(ListURLNode, self).__init__(*args, **kwargs)
+ self.params = {}
+ for (k, v) in kwargs:
+ if k in list_params:
+ self.params[k] = v
+
+ def render(self, context):
+ self.view_name = template.Variable('list_view.view')
+ str = super(ListURLNode, self).render(context)
+ if str == '':
+ return str
+ params = []
+ try:
+ qs_var = template.Variable('list_view.params')
+ params = dict(qs_var.resolve(context))
+ except Exception:
+ pass
+
+ params.update(self.params)
+
+ if not params:
+ return str
+
+ return str + '?' + '&'.join(['%s=%s' % (k, escape(v)) \
+ for (k, v) in params.iteritems()])
+
+@register.tag
+def listurl(parser, token):
+ bits = token.contents.split(' ', 1)
+ if len(bits) < 1:
+ raise TemplateSyntaxError("'%s' takes at least one argument"
+ " (path to a view)" % bits[0])
+ args = ['']
+ kwargs = {}
+ if len(bits) > 1:
+ for arg in bits[2].split(','):
+ if '=' in arg:
+ k, v = arg.split('=', 1)
+ k = k.strip()
+ kwargs[k] = parser.compile_filter(v)
+ else:
+ args.append(parser.compile_filter(arg))
+ return PatchworkURLNode(bits[1], args, kwargs)
+
diff --git a/apps/patchwork/templatetags/syntax.py b/apps/patchwork/templatetags/syntax.py
new file mode 100644
index 0000000..a538062
--- /dev/null
+++ b/apps/patchwork/templatetags/syntax.py
@@ -0,0 +1,72 @@
+# 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
+
+from django import template
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+import re
+
+register = template.Library()
+
+def _compile(t):
+ (r, str) = t
+ return (re.compile(r, re.M | re.I), str)
+
+_patch_span_res = map(_compile, [
+ ('^(Index:?|diff|\-\-\-|\+\+\+|\*\*\*) .*$', 'p_header'),
+ ('^\+.*$', 'p_add'),
+ ('^-.*$', 'p_del'),
+ ('^!.*$', 'p_mod'),
+ ])
+
+_patch_chunk_re = \
+ re.compile('^(@@ \-\d+(?:,\d+)? \+\d+(?:,\d+)? @@)(.*)$', re.M | re.I)
+
+_comment_span_res = map(_compile, [
+ ('^\s*Signed-off-by: .*$', 'signed-off-by'),
+ ('^\s*Acked-by: .*$', 'acked-by'),
+ ('^\s*From: .*$', 'from'),
+ ('^\s*&gt;.*$', 'quote'),
+ ])
+
+_span = '<span class="%s">%s</span>'
+
+@register.filter
+def patchsyntax(patch):
+ content = escape(patch.content)
+
+ for (r,cls) in _patch_span_res:
+ content = r.sub(lambda x: _span % (cls, x.group(0)), content)
+
+ content = _patch_chunk_re.sub( \
+ lambda x: \
+ _span % ('p_chunk', x.group(1)) + ' ' + \
+ _span % ('p_context', x.group(2)), \
+ content)
+
+ return mark_safe(content)
+
+@register.filter
+def commentsyntax(comment):
+ content = escape(comment.content)
+
+ for (r,cls) in _comment_span_res:
+ content = r.sub(lambda x: _span % (cls, x.group(0)), content)
+
+ return mark_safe(content)
diff --git a/apps/patchwork/urls.py b/apps/patchwork/urls.py
new file mode 100644
index 0000000..4a7ccb1
--- /dev/null
+++ b/apps/patchwork/urls.py
@@ -0,0 +1,61 @@
+# 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
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ # Example:
+ (r'^$', 'patchwork.views.projects'),
+ (r'^project/(?P<project_id>[^/]+)/list/$', 'patchwork.views.patch.list'),
+ (r'^project/(?P<project_id>[^/]+)/$', 'patchwork.views.project'),
+
+ # patch views
+ (r'^patch/(?P<patch_id>\d+)/$', 'patchwork.views.patch.patch'),
+ (r'^patch/(?P<patch_id>\d+)/raw/$', 'patchwork.views.patch.content'),
+ (r'^patch/(?P<patch_id>\d+)/mbox/$', 'patchwork.views.patch.mbox'),
+
+ # registration process
+ (r'^register/$', 'patchwork.views.user.register'),
+ (r'^register/confirm/(?P<key>[^/]+)/$',
+ 'patchwork.views.user.register_confirm'),
+
+ (r'^login/$', 'patchwork.views.user.login'),
+ (r'^logout/$', 'patchwork.views.user.logout'),
+
+ # logged-in user stuff
+ (r'^user/$', 'patchwork.views.user.profile'),
+ (r'^user/todo/$', 'patchwork.views.user.todo_lists'),
+ (r'^user/todo/(?P<project_id>[^/]+)/$', 'patchwork.views.user.todo_list'),
+
+ (r'^user/bundle/(?P<bundle_id>[^/]+)/$',
+ 'patchwork.views.bundle.bundle'),
+ (r'^user/bundle/(?P<bundle_id>[^/]+)/mbox/$',
+ 'patchwork.views.bundle.mbox'),
+
+ (r'^user/link/$', 'patchwork.views.user.link'),
+ (r'^user/link/(?P<key>[^/]+)/$', 'patchwork.views.user.link_confirm'),
+ (r'^user/unlink/(?P<person_id>[^/]+)/$', 'patchwork.views.user.unlink'),
+
+ # public view for bundles
+ (r'^bundle/(?P<username>[^/]*)/(?P<bundlename>[^/]*)/$',
+ 'patchwork.views.bundle.public'),
+
+ # submitter autocomplete
+ (r'^submitter/$', 'patchwork.views.submitter_complete'),
+)
diff --git a/apps/patchwork/utils.py b/apps/patchwork/utils.py
new file mode 100644
index 0000000..7cf88bc
--- /dev/null
+++ b/apps/patchwork/utils.py
@@ -0,0 +1,193 @@
+# 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
+
+
+from patchwork.forms import MultiplePatchForm
+from patchwork.models import Bundle, Project, State
+from django.conf import settings
+from django.shortcuts import render_to_response, get_object_or_404
+
+def get_patch_ids(d, prefix = 'patch_id'):
+ ids = []
+
+ for (k, v) in d.items():
+ a = k.split(':')
+ if len(a) != 2:
+ continue
+ if a[0] != prefix:
+ continue
+ if not v:
+ continue
+ ids.append(a[1])
+
+ return ids
+
+class Order(object):
+ order_map = {
+ 'date': 'date',
+ 'name': 'name',
+ 'state': 'state__ordering',
+ 'submitter': 'submitter__name'
+ }
+ default_order = 'date'
+
+ def __init__(self, str = None):
+ self.reversed = False
+
+ if str is None or str == '':
+ self.order = self.default_order
+ return
+
+ reversed = False
+ if str[0] == '-':
+ str = str[1:]
+ reversed = True
+
+ if str not in self.order_map.keys():
+ self.order = self.default_order
+ return
+
+ self.order = str
+ self.reversed = reversed
+
+ def __str__(self):
+ str = self.order
+ if self.reversed:
+ str = '-' + str
+ return str
+
+ def name(self):
+ return self.order
+
+ def reversed_name(self):
+ if self.reversed:
+ return self.order
+ else:
+ return '-' + self.order
+
+ def query(self):
+ q = self.order_map[self.order]
+ if self.reversed:
+ q = '-' + q
+ return q
+
+bundle_actions = ['create', 'add', 'remove']
+def set_bundle(user, action, data, patches, context):
+ # set up the bundle
+ bundle = None
+ if action == 'create':
+ bundle = Bundle(owner = user, project = project,
+ name = data['bundle_name'])
+ bundle.save()
+ str = 'added to new bundle "%s"' % bundle.name
+ auth_required = False
+
+ elif action =='add':
+ bundle = get_object_or_404(Bundle, id = data['bundle_id'])
+ str = 'added to bundle "%s"' % bundle.name
+ auth_required = False
+
+ elif action =='remove':
+ bundle = get_object_or_404(Bundle, id = data['removed_bundle_id'])
+ str = 'removed from bundle "%s"' % bundle.name
+ auth_required = False
+
+ if not bundle:
+ return ['no such bundle']
+
+ for patch in patches:
+ if action == 'create' or action == 'add':
+ bundle.patches.add(patch)
+
+ elif action == 'remove':
+ bundle.patches.remove(patch)
+
+ if len(patches) > 0:
+ if len(patches) == 1:
+ str = 'patch ' + str
+ else:
+ str = 'patches ' + str
+ context.add_message(str)
+
+ bundle.save()
+
+ return []
+
+
+def set_patches(user, action, data, patches, context):
+ errors = []
+ form = MultiplePatchForm(data = data)
+
+ try:
+ project = Project.objects.get(id = data['project'])
+ except:
+ errors = ['No such project']
+ return (errors, form)
+
+ str = ''
+
+ print "action: ", action
+
+ # this may be a bundle action, which doesn't modify a patch. in this
+ # case, don't require a valid form, or patch editing permissions
+ if action in bundle_actions:
+ errors = set_bundle(user, action, data, patches, context)
+ return (errors, form)
+
+ if not form.is_valid():
+ errors = ['The submitted form data was invalid']
+ return (errors, form)
+
+ for patch in patches:
+ if not patch.is_editable(user):
+ errors.append('You don\'t have permissions to edit the ' + \
+ 'patch "%s"' \
+ % patch.name)
+ continue
+
+ if action == 'update':
+ form.save(patch)
+ str = 'updated'
+
+ elif action == 'ack':
+ pass
+
+ elif action == 'archive':
+ patch.archived = True
+ patch.save()
+ str = 'archived'
+
+ elif action == 'unarchive':
+ patch.archived = True
+ patch.save()
+ str = 'un-archived'
+
+ elif action == 'delete':
+ patch.delete()
+ str = 'un-archived'
+
+
+ if len(patches) > 0:
+ if len(patches) == 1:
+ str = 'patch ' + str
+ else:
+ str = 'patches ' + str
+ context.add_message(str)
+
+ return (errors, form)
diff --git a/apps/patchwork/views/__init__.py b/apps/patchwork/views/__init__.py
new file mode 100644
index 0000000..2636d29
--- /dev/null
+++ b/apps/patchwork/views/__init__.py
@@ -0,0 +1,90 @@
+# 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
+
+
+from base import *
+from patchwork.utils import Order, get_patch_ids, set_patches
+from patchwork.paginator import Paginator
+from patchwork.forms import MultiplePatchForm
+
+def generic_list(request, project, view,
+ view_args = {}, filter_settings = [], patches = None):
+
+ context = PatchworkRequestContext(request,
+ list_view = view,
+ list_view_params = view_args)
+
+ context.project = project
+ order = Order(request.REQUEST.get('order'))
+
+ form = MultiplePatchForm(project)
+
+ if request.method == 'POST' and \
+ request.POST.get('form') == 'patchlistform':
+ action = request.POST.get('action', None)
+ if action:
+ action = action.lower()
+
+ # special case: the user may have hit enter in the 'create bundle'
+ # text field, so if non-empty, assume the create action:
+ if request.POST.get('bundle_name', False):
+ action = 'create'
+
+ ps = []
+ for patch_id in get_patch_ids(request.POST):
+ try:
+ patch = Patch.objects.get(id = patch_id)
+ except Patch.DoesNotExist:
+ pass
+ ps.append(patch)
+
+ (errors, form) = set_patches(request.user, action, request.POST, \
+ ps, context)
+ if errors:
+ context['errors'] = errors
+
+ if not (request.user.is_authenticated() and \
+ project in request.user.get_profile().maintainer_projects.all()):
+ form = None
+
+ for (filterclass, setting) in filter_settings:
+ if isinstance(setting, dict):
+ context.filters.set_status(filterclass, **setting)
+ elif isinstance(setting, list):
+ context.filters.set_status(filterclass, *setting)
+ else:
+ context.filters.set_status(filterclass, setting)
+
+ if not patches:
+ patches = Patch.objects.filter(project=project)
+
+ patches = context.filters.apply(patches)
+ patches = patches.order_by(order.query())
+
+ paginator = Paginator(request, patches)
+
+ context.update({
+ 'page': paginator.current_page,
+ 'patchform': form,
+ 'project': project,
+ 'order': order,
+ })
+
+ return context
+
diff --git a/apps/patchwork/views/base.py b/apps/patchwork/views/base.py
new file mode 100644
index 0000000..16fa5db
--- /dev/null
+++ b/apps/patchwork/views/base.py
@@ -0,0 +1,66 @@
+# 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
+
+
+from patchwork.models import Patch, Project, Person, RegistrationRequest
+from patchwork.filters import Filters
+from patchwork.forms import RegisterForm, LoginForm, PatchForm
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponse, HttpResponseRedirect
+from django.db import transaction
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+import django.core.urlresolvers
+from patchwork.requestcontext import PatchworkRequestContext
+from django.core import serializers
+
+def projects(request):
+ context = PatchworkRequestContext(request)
+ projects = Project.objects.all()
+
+ if projects.count() == 1:
+ return HttpResponseRedirect(
+ django.core.urlresolvers.reverse('patchwork.views.patch.list',
+ kwargs = {'project_id': projects[0].linkname}))
+
+ context['projects'] = projects
+ return render_to_response('patchwork/projects.html', context)
+
+def project(request, project_id):
+ context = PatchworkRequestContext(request)
+ project = get_object_or_404(Project, linkname = project_id)
+ context.project = project
+
+ context['maintainers'] = User.objects.filter( \
+ userprofile__maintainer_projects = project)
+ context['n_patches'] = Patch.objects.filter(project = project,
+ archived = False).count()
+ context['n_archived_patches'] = Patch.objects.filter(project = project,
+ archived = True).count()
+
+ return render_to_response('patchwork/project.html', context)
+
+def submitter_complete(request):
+ search = request.GET.get('q', '')
+ response = HttpResponse(mimetype = "text/plain")
+ if len(search) > 3:
+ queryset = Person.objects.filter(name__icontains = search)
+ json_serializer = serializers.get_serializer("json")()
+ json_serializer.serialize(queryset, ensure_ascii=False, stream=response)
+ return response
diff --git a/apps/patchwork/views/bundle.py b/apps/patchwork/views/bundle.py
new file mode 100644
index 0000000..be6a937
--- /dev/null
+++ b/apps/patchwork/views/bundle.py
@@ -0,0 +1,158 @@
+# 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
+
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth.models import User
+from django.shortcuts import render_to_response, get_object_or_404
+from patchwork.requestcontext import PatchworkRequestContext
+from django.http import HttpResponse, HttpResponseRedirect
+import django.core.urlresolvers
+from patchwork.models import Patch, Bundle, Project
+from patchwork.utils import get_patch_ids
+from patchwork.forms import BundleForm
+from patchwork.views import generic_list
+from patchwork.filters import DelegateFilter
+from patchwork.paginator import Paginator
+
+@login_required
+def setbundle(request):
+ context = PatchworkRequestContext(request)
+
+ bundle = None
+
+ if request.method == 'POST':
+ action = request.POST.get('action', None)
+ if action is None:
+ pass
+ elif action == 'create':
+ project = get_object_or_404(Project,
+ id = request.POST.get('project'))
+ bundle = Bundle(owner = request.user, project = project,
+ name = request.POST['name'])
+ bundle.save()
+ patch_id = request.POST.get('patch_id', None)
+ if patch_id:
+ patch = get_object_or_404(Patch, id = patch_id)
+ bundle.patches.add(patch)
+ bundle.save()
+ elif action == 'add':
+ bundle = get_object_or_404(Bundle,
+ owner = request.user, id = request.POST['id'])
+ bundle.save()
+
+ patch_id = request.get('patch_id', None)
+ if patch_id:
+ patch_ids = patch_id
+ else:
+ patch_ids = get_patch_ids(request.POST)
+
+ for id in patch_ids:
+ try:
+ patch = Patch.objects.get(id = id)
+ bundle.patches.add(patch)
+ except ex:
+ pass
+
+ bundle.save()
+ elif action == 'delete':
+ try:
+ bundle = Bundle.objects.get(owner = request.user,
+ id = request.POST['id'])
+ bundle.delete()
+ except Exception:
+ pass
+
+ bundle = None
+
+ else:
+ bundle = get_object_or_404(Bundle, owner = request.user,
+ id = request.POST['bundle_id'])
+
+ if 'error' in context:
+ pass
+
+ if bundle:
+ return HttpResponseRedirect(
+ django.core.urlresolvers.reverse(
+ 'patchwork.views.bundle.bundle',
+ kwargs = {'bundle_id': bundle.id}
+ )
+ )
+ else:
+ return HttpResponseRedirect(
+ django.core.urlresolvers.reverse(
+ 'patchwork.views.bundle.list')
+ )
+
+@login_required
+def bundle(request, bundle_id):
+ bundle = get_object_or_404(Bundle, id = bundle_id)
+ filter_settings = [(DelegateFilter, DelegateFilter.AnyDelegate)]
+
+ if request.method == 'POST' and request.POST.get('form') == 'bundle':
+ action = request.POST.get('action', '').lower()
+ if action == 'delete':
+ bundle.delete()
+ return HttpResponseRedirect(
+ django.core.urlresolvers.reverse(
+ 'patchwork.views.user.profile')
+ )
+ elif action == 'update':
+ form = BundleForm(request.POST, instance = bundle)
+ if form.is_valid():
+ form.save()
+ else:
+ form = BundleForm(instance = bundle)
+
+ context = generic_list(request, bundle.project,
+ 'patchwork.views.bundle.bundle',
+ view_args = {'bundle_id': bundle_id},
+ filter_settings = filter_settings,
+ patches = bundle.patches.all())
+
+ context['bundle'] = bundle
+ context['bundleform'] = form
+
+ return render_to_response('patchwork/bundle.html', context)
+
+@login_required
+def mbox(request, bundle_id):
+ bundle = get_object_or_404(Bundle, id = bundle_id)
+ response = HttpResponse(mimetype='text/plain')
+ response.write(bundle.mbox())
+ return response
+
+def public(request, username, bundlename):
+ user = get_object_or_404(User, username = username)
+ bundle = get_object_or_404(Bundle, name = bundlename, public = True)
+ filter_settings = [(DelegateFilter, DelegateFilter.AnyDelegate)]
+ context = generic_list(request, bundle.project,
+ 'patchwork.views.bundle.public',
+ view_args = {'username': username, 'bundlename': bundlename},
+ filter_settings = filter_settings,
+ patches = bundle.patches.all())
+
+ context.update({'bundle': bundle,
+ 'user': user});
+ return render_to_response('patchwork/bundle-public.html', context)
+
+@login_required
+def set_patches(request):
+ context = PatchworkRequestContext(request)
+
diff --git a/apps/patchwork/views/patch.py b/apps/patchwork/views/patch.py
new file mode 100644
index 0000000..d509e28
--- /dev/null
+++ b/apps/patchwork/views/patch.py
@@ -0,0 +1,180 @@
+# 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
+
+
+from patchwork.models import Patch, Project, Person, RegistrationRequest, Bundle
+from patchwork.filters import Filters
+from patchwork.forms import RegisterForm, LoginForm, PatchForm, MultiplePatchForm, CreateBundleForm
+from patchwork.utils import get_patch_ids, set_patches, Order
+from patchwork.requestcontext import PatchworkRequestContext
+from django.shortcuts import render_to_response, get_object_or_404
+from django.http import HttpResponse, HttpResponseRedirect, \
+ HttpResponseForbidden
+from django.contrib.auth.models import User
+from django.contrib.auth.decorators import login_required
+from django.contrib.auth import authenticate, login
+import django.core.urlresolvers
+from patchwork.paginator import Paginator
+from patchwork.views import generic_list
+
+def patch(request, patch_id):
+ context = PatchworkRequestContext(request)
+ patch = get_object_or_404(Patch, id=patch_id)
+ context.project = patch.project
+ editable = patch.is_editable(request.user)
+ messages = []
+
+ form = None
+ createbundleform = None
+
+ if editable:
+ form = PatchForm(instance = patch)
+ if request.user.is_authenticated():
+ createbundleform = CreateBundleForm()
+
+ if request.method == 'POST':
+ action = request.POST.get('action', None)
+ if action:
+ action = action.lower()
+
+ if action == 'createbundle':
+ bundle = Bundle(owner = request.user, project = patch.project)
+ createbundleform = CreateBundleForm(instance = bundle,
+ data = request.POST)
+ if createbundleform.is_valid():
+ createbundleform.save()
+ bundle.patches.add(patch)
+ bundle.save()
+ createbundleform = CreateBundleForm()
+ context.add_message('Bundle %s created' % bundle.name)
+
+ elif action == 'addtobundle':
+ bundle = get_object_or_404(Bundle, id = \
+ request.POST.get('bundle_id'))
+ bundle.patches.add(patch)
+ bundle.save()
+ context.add_message('Patch added to bundle "%s"' % bundle.name)
+
+ # all other actions require edit privs
+ elif not editable:
+ return HttpResponseForbidden()
+
+ elif action is None:
+ form = PatchForm(data = request.POST, instance = patch)
+ if form.is_valid():
+ form.save()
+ context.add_message('Patch updated')
+
+ elif action == 'archive':
+ patch.archived = True
+ patch.save()
+ context.add_message('Patch archived')
+
+ elif action == 'unarchive':
+ patch.archived = False
+ patch.save()
+ context.add_message('Patch un-archived')
+
+ elif action == 'ack':
+ pass
+
+ elif action == 'delete':
+ patch.delete()
+
+
+ context['patch'] = patch
+ context['patchform'] = form
+ context['createbundleform'] = createbundleform
+ context['project'] = patch.project
+
+ return render_to_response('patchwork/patch.html', context)
+
+def content(request, patch_id):
+ patch = get_object_or_404(Patch, id=patch_id)
+ response = HttpResponse(mimetype="text/x-patch")
+ response.write(patch.content)
+ response['Content-Disposition'] = 'attachment; filename=' + \
+ patch.filename().replace(';', '').replace('\n', '')
+ return response
+
+def mbox(request, patch_id):
+ patch = get_object_or_404(Patch, id=patch_id)
+ response = HttpResponse(mimetype="text/plain")
+ response.write(patch.mbox().as_string(True))
+ response['Content-Disposition'] = 'attachment; filename=' + \
+ patch.filename().replace(';', '').replace('\n', '')
+ return response
+
+
+def list(request, project_id):
+ project = get_object_or_404(Project, linkname=project_id)
+ context = generic_list(request, project, 'patchwork.views.patch.list',
+ view_args = {'project_id': project.linkname})
+ return render_to_response('patchwork/list.html', context)
+
+ context = PatchworkRequestContext(request,
+ list_view = 'patchwork.views.patch.list',
+ list_view_params = {'project_id': project_id})
+ order = get_order(request)
+ project = get_object_or_404(Project, linkname=project_id)
+ context.project = project
+
+ form = None
+ errors = []
+
+ if request.method == 'POST':
+ action = request.POST.get('action', None)
+ if action:
+ action = action.lower()
+
+ # special case: the user may have hit enter in the 'create bundle'
+ # text field, so if non-empty, assume the create action:
+ if request.POST.get('bundle_name', False):
+ action = 'create'
+
+ ps = []
+ for patch_id in get_patch_ids(request.POST):
+ try:
+ patch = Patch.objects.get(id = patch_id)
+ except Patch.DoesNotExist:
+ pass
+ ps.append(patch)
+
+ (errors, form) = set_patches(request.user, action, request.POST, ps)
+ if errors:
+ context['errors'] = errors
+
+
+ elif request.user.is_authenticated() and \
+ project in request.user.get_profile().maintainer_projects.all():
+ form = MultiplePatchForm(project)
+
+ patches = Patch.objects.filter(project=project).order_by(order)
+ patches = context.filters.apply(patches)
+
+ paginator = Paginator(request, patches)
+
+ context.update({
+ 'page': paginator.current_page,
+ 'patchform': form,
+ 'project': project,
+ 'errors': errors,
+ })
+
+ return render_to_response('patchwork/list.html', context)
diff --git a/apps/patchwork/views/user.py b/apps/patchwork/views/user.py
new file mode 100644
index 0000000..223cfc6
--- /dev/null
+++ b/apps/patchwork/views/user.py
@@ -0,0 +1,201 @@
+# 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
+
+
+from django.contrib.auth.decorators import login_required
+from patchwork.requestcontext import PatchworkRequestContext
+from django.shortcuts import render_to_response, get_object_or_404
+from django.contrib import auth
+from django.http import HttpResponse, HttpResponseRedirect
+from patchwork.models import Project, Patch, Bundle, Person, \
+ RegistrationRequest, UserProfile, UserPersonConfirmation, State
+from patchwork.forms import RegisterForm, LoginForm, MultiplePatchForm, \
+ UserProfileForm, UserPersonLinkForm
+from patchwork.utils import Order, get_patch_ids, set_patches
+from patchwork.filters import DelegateFilter
+from patchwork.paginator import Paginator
+from patchwork.views import generic_list
+import django.core.urlresolvers
+
+def register(request):
+ context = PatchworkRequestContext(request)
+ template = 'patchwork/register.html'
+
+ if request.method != 'POST':
+ form = RegisterForm()
+ context['form'] = form
+ return render_to_response(template, context)
+
+ reg_req = RegistrationRequest()
+ form = RegisterForm(instance = reg_req, data = request.POST)
+
+ if form.is_valid():
+ form.save()
+ context['request'] = reg_req
+ else:
+ context['form'] = form
+
+ return render_to_response(template, context)
+
+def register_confirm(request, key):
+ context = PatchworkRequestContext(request)
+ req = get_object_or_404(RegistrationRequest, key = key)
+ req.create_user()
+ user = auth.authenticate(username = req.username, password = req.password)
+ auth.login(request, user)
+
+ return render_to_response('patchwork/register-confirm.html', context)
+
+def login(request):
+ context = PatchworkRequestContext(request)
+ template = 'patchwork/login.html'
+ error = None
+
+ if request.method == 'POST':
+ form = LoginForm(request.POST)
+ context['form'] = form
+
+ if not form.is_valid():
+ return render_to_response(template, context)
+
+ data = form.cleaned_data
+ user = auth.authenticate(username = data['username'],
+ password = data['password'])
+
+ if user is not None and user.is_active:
+ auth.login(request, user)
+ url = request.POST.get('next', None) or \
+ django.core.urlresolvers.reverse( \
+ 'patchwork.views.user.profile')
+ return HttpResponseRedirect(url)
+
+ context['error'] = 'Invalid username or password'
+
+ else:
+ context['form'] = LoginForm()
+
+ return render_to_response(template, context)
+
+def logout(request):
+ auth.logout(request)
+ return render_to_response('patchwork/logout.html')
+
+@login_required
+def profile(request):
+ context = PatchworkRequestContext(request)
+
+ if request.method == 'POST':
+ form = UserProfileForm(instance = request.user.get_profile(),
+ data = request.POST)
+ if form.is_valid():
+ form.save()
+ else:
+ form = UserProfileForm(instance = request.user.get_profile())
+
+ context.project = request.user.get_profile().primary_project
+ context['bundles'] = Bundle.objects.filter(owner = request.user)
+ context['profileform'] = form
+
+ people = Person.objects.filter(user = request.user)
+ context['linked_emails'] = people
+ context['linkform'] = UserPersonLinkForm()
+
+ return render_to_response('patchwork/profile.html', context)
+
+@login_required
+def link(request):
+ context = PatchworkRequestContext(request)
+
+ form = UserPersonLinkForm(request.POST)
+ if request.method == 'POST':
+ form = UserPersonLinkForm(request.POST)
+ if form.is_valid():
+ conf = UserPersonConfirmation(user = request.user,
+ email = form.cleaned_data['email'])
+ conf.save()
+ context['confirmation'] = conf
+
+ context['linkform'] = form
+
+ return render_to_response('patchwork/user-link.html', context)
+
+@login_required
+def link_confirm(request, key):
+ context = PatchworkRequestContext(request)
+ confirmation = get_object_or_404(UserPersonConfirmation, key = key)
+
+ errors = confirmation.confirm()
+ if errors:
+ context['errors'] = errors
+ else:
+ context['person'] = Person.objects.get(email = confirmation.email)
+
+ confirmation.delete()
+
+ return render_to_response('patchwork/user-link-confirm.html', context)
+
+@login_required
+def unlink(request, person_id):
+ context = PatchworkRequestContext(request)
+ person = get_object_or_404(Person, id = person_id)
+
+ if request.method == 'POST':
+ if person.email != request.user.email:
+ person.user = None
+ person.save()
+
+ url = django.core.urlresolvers.reverse('patchwork.views.user.profile')
+ return HttpResponseRedirect(url)
+
+
+@login_required
+def todo_lists(request):
+ todo_lists = []
+
+ for project in Project.objects.all():
+ patches = request.user.get_profile().todo_patches(project = project)
+ if not patches.count():
+ continue
+
+ todo_lists.append({'project': project, 'n_patches': patches.count()})
+
+ if len(todo_lists) == 1:
+ return todo_list(request, todo_lists[0]['project'].linkname)
+
+ context = PatchworkRequestContext(request)
+ context['todo_lists'] = todo_lists
+ context.project = request.user.get_profile().primary_project
+ return render_to_response('patchwork/todo-lists.html', context)
+
+@login_required
+def todo_list(request, project_id):
+ project = get_object_or_404(Project, linkname = project_id)
+ patches = request.user.get_profile().todo_patches(project = project)
+ filter_settings = [(DelegateFilter,
+ {'delegate': request.user})]
+
+ context = generic_list(request, project,
+ 'patchwork.views.user.todo_list',
+ view_args = {'project_id': project.linkname},
+ filter_settings = filter_settings,
+ patches = patches)
+
+ context['action_required_states'] = \
+ State.objects.filter(action_required = True).all()
+ return render_to_response('patchwork/todo-list.html', context)
diff --git a/apps/settings.py b/apps/settings.py
new file mode 100644
index 0000000..0d74b10
--- /dev/null
+++ b/apps/settings.py
@@ -0,0 +1,94 @@
+# Django settings for patchwork project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ ('Jeremy Kerr', 'jk@ozlabs.org'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'postgresql' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = 'patchwork' # Or path to database file if using sqlite3.
+DATABASE_USER = '' # Not used with sqlite3.
+DATABASE_PASSWORD = '' # Not used with sqlite3.
+DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+# although not all variations may be possible on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'Australia/Canberra'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-au'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = '/srv/patchwork/lib/python/django/contrib/admin/media'
+
+# URL that handles the media served from MEDIA_ROOT.
+# Example: "http://media.lawrence.com"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '00000000000000000000000000000000000000000000000000'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'apps.urls'
+
+LOGIN_URL = '/patchwork/login'
+
+TEMPLATE_DIRS = (
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ '/srv/patchwork/templates'
+)
+TEMPLATE_CONTEXT_PROCESSORS = (
+ "django.core.context_processors.auth",
+ "django.core.context_processors.debug",
+ "django.core.context_processors.i18n",
+ "django.core.context_processors.media")
+
+AUTH_PROFILE_MODULE = "patchwork.userprofile"
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.admin',
+ 'patchwork',
+)
+
+DEFAULT_PATCHES_PER_PAGE = 100
diff --git a/apps/urls.py b/apps/urls.py
new file mode 100644
index 0000000..e11cbd9
--- /dev/null
+++ b/apps/urls.py
@@ -0,0 +1,35 @@
+# 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
+
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ # Example:
+ (r'^', include('patchwork.urls')),
+
+ # Uncomment this for admin:
+ (r'^admin/', include('django.contrib.admin.urls')),
+
+ (r'^css/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/css'}),
+ (r'^js/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/js'}),
+ (r'^images/(?P<path>.*)$', 'django.views.static.serve',
+ {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/images'}),
+)