From c561ebe710d6e6a43aa4afc6c2036a215378ce87 Mon Sep 17 00:00:00 2001 From: Jeremy Kerr Date: Thu, 21 Aug 2008 09:38:06 +0800 Subject: [PATCH] Inital commit Signed-off-by: Jeremy Kerr --- apps/__init__.py | 0 apps/manage.py | 14 + apps/patchwork/__init__.py | 0 apps/patchwork/bin/parsemail-batch.sh | 49 ++ apps/patchwork/bin/parsemail.py | 263 +++++++++++ apps/patchwork/bin/parsemail.sh | 28 ++ apps/patchwork/bin/patchparser.py | 158 +++++++ apps/patchwork/bin/setup.py | 29 ++ apps/patchwork/bin/update-patchwork-status.py | 70 +++ apps/patchwork/context_processors.py | 32 ++ apps/patchwork/filters.py | 433 ++++++++++++++++++ apps/patchwork/forms.py | 213 +++++++++ apps/patchwork/models.py | 362 +++++++++++++++ apps/patchwork/paginator.py | 88 ++++ apps/patchwork/parser.py | 206 +++++++++ apps/patchwork/requestcontext.py | 82 ++++ apps/patchwork/sql/project.sql | 6 + apps/patchwork/sql/state.sql | 20 + apps/patchwork/templatetags/__init__.py | 0 apps/patchwork/templatetags/filter.py | 36 ++ apps/patchwork/templatetags/listurl.py | 136 ++++++ apps/patchwork/templatetags/order.py | 66 +++ apps/patchwork/templatetags/patch.py | 65 +++ apps/patchwork/templatetags/person.py | 40 ++ apps/patchwork/templatetags/pwurl.py | 76 +++ apps/patchwork/templatetags/syntax.py | 72 +++ apps/patchwork/urls.py | 61 +++ apps/patchwork/utils.py | 193 ++++++++ apps/patchwork/views/__init__.py | 90 ++++ apps/patchwork/views/base.py | 66 +++ apps/patchwork/views/bundle.py | 158 +++++++ apps/patchwork/views/patch.py | 180 ++++++++ apps/patchwork/views/user.py | 201 ++++++++ apps/settings.py | 94 ++++ apps/urls.py | 35 ++ docs/INSTALL | 143 ++++++ htdocs/css/style.css | 417 +++++++++++++++++ htdocs/images/filter-add.png | Bin 0 -> 397 bytes htdocs/images/filter-remove.png | Bin 0 -> 320 bytes htdocs/images/title-background.png | Bin 0 -> 246 bytes htdocs/js/autocomplete.js | 43 ++ htdocs/js/filters.js | 78 ++++ htdocs/js/people.js | 5 + lib/apache2/patchwork.fastcgi.conf | 17 + lib/apache2/patchwork.mod_python.conf | 22 + lib/sql/grant-all.sql | 68 +++ templates/patchwork/base.html | 77 ++++ templates/patchwork/bundle-public.html | 12 + templates/patchwork/bundle.html | 39 ++ templates/patchwork/filters.html | 173 +++++++ templates/patchwork/list.html | 24 + templates/patchwork/login.html | 26 ++ templates/patchwork/logout.html | 8 + templates/patchwork/pagination.html | 45 ++ templates/patchwork/patch-form.html | 87 ++++ templates/patchwork/patch-list.html | 185 ++++++++ templates/patchwork/patch.html | 214 +++++++++ templates/patchwork/patchlist.html | 36 ++ templates/patchwork/profile.html | 114 +++++ templates/patchwork/project.html | 32 ++ templates/patchwork/projects.html | 21 + templates/patchwork/register-confirm.html | 13 + templates/patchwork/register.html | 122 +++++ templates/patchwork/todo-list.html | 17 + templates/patchwork/todo-lists.html | 29 ++ templates/patchwork/user-link-confirm.html | 19 + templates/patchwork/user-link.html | 30 ++ 67 files changed, 5738 insertions(+) create mode 100644 apps/__init__.py create mode 100755 apps/manage.py create mode 100644 apps/patchwork/__init__.py create mode 100644 apps/patchwork/bin/parsemail-batch.sh create mode 100755 apps/patchwork/bin/parsemail.py create mode 100755 apps/patchwork/bin/parsemail.sh create mode 100644 apps/patchwork/bin/patchparser.py create mode 100755 apps/patchwork/bin/setup.py create mode 100755 apps/patchwork/bin/update-patchwork-status.py create mode 100644 apps/patchwork/context_processors.py create mode 100644 apps/patchwork/filters.py create mode 100644 apps/patchwork/forms.py create mode 100644 apps/patchwork/models.py create mode 100644 apps/patchwork/paginator.py create mode 100644 apps/patchwork/parser.py create mode 100644 apps/patchwork/requestcontext.py create mode 100644 apps/patchwork/sql/project.sql create mode 100644 apps/patchwork/sql/state.sql create mode 100644 apps/patchwork/templatetags/__init__.py create mode 100644 apps/patchwork/templatetags/filter.py create mode 100644 apps/patchwork/templatetags/listurl.py create mode 100644 apps/patchwork/templatetags/order.py create mode 100644 apps/patchwork/templatetags/patch.py create mode 100644 apps/patchwork/templatetags/person.py create mode 100644 apps/patchwork/templatetags/pwurl.py create mode 100644 apps/patchwork/templatetags/syntax.py create mode 100644 apps/patchwork/urls.py create mode 100644 apps/patchwork/utils.py create mode 100644 apps/patchwork/views/__init__.py create mode 100644 apps/patchwork/views/base.py create mode 100644 apps/patchwork/views/bundle.py create mode 100644 apps/patchwork/views/patch.py create mode 100644 apps/patchwork/views/user.py create mode 100644 apps/settings.py create mode 100644 apps/urls.py create mode 100644 docs/INSTALL create mode 100644 htdocs/css/style.css create mode 100644 htdocs/images/filter-add.png create mode 100644 htdocs/images/filter-remove.png create mode 100644 htdocs/images/title-background.png create mode 100644 htdocs/js/autocomplete.js create mode 100644 htdocs/js/filters.js create mode 100644 htdocs/js/people.js create mode 100644 lib/apache2/patchwork.fastcgi.conf create mode 100644 lib/apache2/patchwork.mod_python.conf create mode 100644 lib/sql/grant-all.sql create mode 100644 templates/patchwork/base.html create mode 100644 templates/patchwork/bundle-public.html create mode 100644 templates/patchwork/bundle.html create mode 100644 templates/patchwork/filters.html create mode 100644 templates/patchwork/list.html create mode 100644 templates/patchwork/login.html create mode 100644 templates/patchwork/logout.html create mode 100644 templates/patchwork/pagination.html create mode 100644 templates/patchwork/patch-form.html create mode 100644 templates/patchwork/patch-list.html create mode 100644 templates/patchwork/patch.html create mode 100644 templates/patchwork/patchlist.html create mode 100644 templates/patchwork/profile.html create mode 100644 templates/patchwork/project.html create mode 100644 templates/patchwork/projects.html create mode 100644 templates/patchwork/register-confirm.html create mode 100644 templates/patchwork/register.html create mode 100644 templates/patchwork/todo-list.html create mode 100644 templates/patchwork/todo-lists.html create mode 100644 templates/patchwork/user-link-confirm.html create mode 100644 templates/patchwork/user-link.html diff --git a/apps/__init__.py b/apps/__init__.py new file mode 100644 index 0000000..e69de29 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 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 +# +# 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 " >&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 +# +# 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" 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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('%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((' ' % escape(name)) + + '') + + 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 = '' + 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('' %\ + (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 += ('%(label)s' + \ + '    ') % \ + {'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 = '' + + 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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([ '' % \ + (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 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 +# +# 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 '%s' % (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 +# +# 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 += '' % \ + (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 +# +# 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 +# +# 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 +# +# 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 = '%s' % \ + (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 +# +# 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 +# +# 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*>.*$', 'quote'), + ]) + +_span = '%s' + +@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 +# +# 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[^/]+)/list/$', 'patchwork.views.patch.list'), + (r'^project/(?P[^/]+)/$', 'patchwork.views.project'), + + # patch views + (r'^patch/(?P\d+)/$', 'patchwork.views.patch.patch'), + (r'^patch/(?P\d+)/raw/$', 'patchwork.views.patch.content'), + (r'^patch/(?P\d+)/mbox/$', 'patchwork.views.patch.mbox'), + + # registration process + (r'^register/$', 'patchwork.views.user.register'), + (r'^register/confirm/(?P[^/]+)/$', + '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[^/]+)/$', 'patchwork.views.user.todo_list'), + + (r'^user/bundle/(?P[^/]+)/$', + 'patchwork.views.bundle.bundle'), + (r'^user/bundle/(?P[^/]+)/mbox/$', + 'patchwork.views.bundle.mbox'), + + (r'^user/link/$', 'patchwork.views.user.link'), + (r'^user/link/(?P[^/]+)/$', 'patchwork.views.user.link_confirm'), + (r'^user/unlink/(?P[^/]+)/$', 'patchwork.views.user.unlink'), + + # public view for bundles + (r'^bundle/(?P[^/]*)/(?P[^/]*)/$', + '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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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 +# +# 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.*)$', 'django.views.static.serve', + {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/css'}), + (r'^js/(?P.*)$', 'django.views.static.serve', + {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/js'}), + (r'^images/(?P.*)$', 'django.views.static.serve', + {'document_root': '/home/jk/devel/patchwork/pwsite/htdocs/images'}), +) diff --git a/docs/INSTALL b/docs/INSTALL new file mode 100644 index 0000000..da8dd54 --- /dev/null +++ b/docs/INSTALL @@ -0,0 +1,143 @@ +Deploying Patchwork + +Patchwork uses the django framework - there is some background on deploying +django applications here: + + http://www.djangobook.com/en/1.0/chapter20/ + +You'll need the following (applications used for patchwork development are +in brackets): + + * A python interpreter + * djano + * A webserver (apache) + * mod_python or flup + * A database server (postgresql) + +1. Database setup + + At present, I've tested with PostgreSQL and (to a lesser extent) MySQL + database servers. If you have any (positive or negative) experiences with + either, email me. + + For the following commands, a $ prefix signifies that the command should be + entered at your shell prompt, and a > prefix signifies the commant-line + client for your sql server (psql or mysql) + + Create a database for the system, add accounts for two system users: the + web user (the user that your web server runs as) and the mail user (the + user that your mail server runs as). On Ubuntu these are + www-data and nobody, respectively. + + PostgreSQL: + createdb patchwork + createuser www-data + createuser nobody + + MySQL: + $ mysql + > CREATE DATABASE 'patchwork'; + > INSERT INTO user (Host, User) VALUES ('localhost', 'www-data'); + > INSERT INTO user (Host, User) VALUES ('localhost', 'nobody'); + +2. Django setup + + You'll need to customise apps/settings.py to suit your database, and + change the SECRET_KEY variable too. While you're there, change the + following to suit your installation: + + ADMINS, + TIME_ZONE + LANGUAGE_CODE + MEDIA_ROOT + + Then, get patchwork to create its tables in your configured database: + + cd apps/ + ./manage.py syncdb + + And add privileges for your mail and web users: + + Postgresql: + psql -f lib/sql/grant-all.sql patchwork + + +3. Apache setup + +Example apache configuration files are in lib/apache/. + +mod_python: + + This should be the simpler of the two to set up. An example apache + configuration file is in: + + lib/apache/patchwork.mod_python.conf + + However, mod_python and mod_php may not work well together. So, if your + web server is used for serving php files, the fastcgi method may suit + instead. + +fastcgi: + + django has built-in support for fastcgi, which requires the + 'flup' python module. An example configuration is in: + + lib/apache/patchwork.fastcgi.conf + + - this also requires the mod_rewrite apache module to be loaded. + + Once you have apache set up, you can start the fastcgi server with: + + cd /srv/patchwork/apps + ./manage.py runfcgi method=prefork \ + socket=/srv/patchwork/var/fcgi.sock \ + pidfile=/srv/patchwork/var/fcgi.pid + +4. Configure patchwork + Now, you should be able to administer patchwork, by visiting the + URL: + + http://your-host/admin/ + + You'll probably want to do the following: + + * Set up your projects + + +5. Subscribe a local address to the mailing list + + You will need an email address for patchwork to receive email on - for + example - patchwork@, and this address will need to be subscribed to the + list. Depending on the mailing list, you will probably need to confirm the + subscription - temporarily direct the alias to yourself to do this. + +6. Setup your MTA to deliver mail to the parsemail script + + Your MTA will need to deliver mail to the parsemail script in the email/ + directory. (Note, do not use the parsemail.py script directly). Something + like this in /etc/aliases is suitable for postfix: + + patchwork: "|/srv/patchwork/apps/patchwork/bin/parsemail.sh" + + You may need to customise the parsemail.sh script if you haven't installed + patchwork in /srv/patchwork. + + Test that you can deliver a patch to this script: + + sudo -u nobody /srv/patchwork/apps/patchwork/bin/parsemail.sh < mail + + +Some errors: + +* __init__() got an unexpected keyword argument 'max_length' + + - you're running an old version of django. If your distribution doesn't + provide a newer version, just download and extract django into + lib/python/django + +* ERROR: permission denied for relation patchwork_... + + - the user that patchwork is running as (ie, the user of the web-server) + doesn't have access to the patchwork tables in the database. Check that + your web-server user exists in the database, and that it has permissions + to the tables. diff --git a/htdocs/css/style.css b/htdocs/css/style.css new file mode 100644 index 0000000..2d8d628 --- /dev/null +++ b/htdocs/css/style.css @@ -0,0 +1,417 @@ +body { + background-color: white; + color: black; + margin: 0em; + font-size: 9pt; +} + +.floaty { + position: fixed; + left: 0.1em; + top: 17em; +} + + +#title { + background: url('/images/title-background.png') top left repeat-x; + background-color: #786fb4; + width: 100%; + margin: 0px; + padding-top: 0.1em; + padding-bottom: 0.0em; + padding-left: 2em; +} + +#title h1, #title h1 a { + font-size: 16pt; + color: white; +} + +.beta { + font-size: 60%; + vertical-align: sub; + line-height: 2em; +} + +#auth { + border-left: thin solid white; + padding-top: 0em; + padding-left: 2em; + padding-right: 5em; + padding-top: 0.5em; + padding-bottom: 0.5em; + font-size: 90%; + float: right; + color: white; +} +#auth a { + color: white; +} + +#nav { + background: #e8e8e8; + border-bottom: 0.2em solid #786fb4; + font-size: 90%; + padding: 0.2em 0.5em; +} + +#nav a { + text-decoration: underline; +} + + +#content { + padding: 1em; +} + +form { + padding: 0em; + margin: 0em; +} + +a:visited { color: #000000; } +a { color: #786fb4; } + +table { + border-collapse: collapse; +} + +/* messages */ +#messages { + background: #e0e0f0; + margin: 0.5em 1em 0.0em 0.5em; + padding: 0.3em; +} + +#messages .message { + color: green; +} + +/* patch lists */ +table.patchlist { + width: 98%; + border: thin solid black; + padding: 0em 1em; +} + +table.patchlist th { + background: #eeeeee; + border-bottom: thin solid black; + text-align: left; + padding-left: 6px; +} + +table.patchlist td { + padding: 2px 6px 2px 6px; + margin: 0px; + margin-top: 10px; +} + +table.patchlist td.patchlistfilters { + background: #c0c0ff; + border-top: thin solid #; + border-top: thin solid gray; + border-bottom: thin solid black; + font-size: smaller; + +} +table.patchlist tr.odd { + background: #ffffff; +} + +table.patchlist tr.even { + background: #eeeeee; +} + +a.colactive { + color: red; +} + +a.colinactive { + color: black; + text-decoration: none; +} +a.colinactive:hover { + color: red; +} + +div.filters { +} + +/* list pagination */ +.paginator { padding-bottom: 1em; padding-top: 1em; font-size: 80%; } + +.paginator .prev-na, +.paginator .next-na { + padding:.3em; + font-weight: normal; + border: 0.1em solid #c0c0c0; + background-color: #f9f9f9; + color: #a0a0a0; +} + +.paginator .prev a, .paginator .prev a:visited, +.paginator .next a, .paginator .next a:visited { + border: 0.1em solid #b0b0d0; + background-color: #eeeeee; + color: #786fb4; + padding: .3em; + font-weight: bold; +} + +.paginator .prev, .paginator .prev-na { margin-right:.5em; } +.paginator .next, .paginator .next-na { margin-left:.5em; } + +.paginator .page a, .paginator .page a:visited, .paginator .curr { + padding: .25em; + font-weight: bold; + border: 1px solid #b0b0d0; + background-color: #eeeeee; + margin: 0em .25em; + color: #786fb4; +} + +.paginator .curr { + background-color: #b0b0d0; + color:#fff; + border:1px solid #c0c0ff; + font-weight:bold; +} + +.paginator .page a:hover, +.paginator .curr a:hover, +.paginator .prev a:hover, +.paginator .next a:hover { + color: #ffffff; + background-color: #c0c0ff; + border:1px solid #234f32; +} +/* +div.filters h2 { + font-size: 110%; +} + +table.filters tr th, table.filters tr td { + text-align: left; + padding: 0px 10px 0px 10px; + vertical-align: middle; +} +table.filters tr th { + width: 8em; +} + +table.filters tr td { + padding-top: 0.1em; + width: 12em; +} + +table.filters tr td.clearcol { + text-align: right; + width: 16px; +} +*/ + +img { + border: 0; +} + +input { + border: thin solid #909090; +} + +#footer { + padding: 1em; + font-size: small; + text-align: center; + color: #909090; +} + +#footer a { + color: #909090; +} + +/* patch view */ +table.patchmeta th { + text-align: left; +} + +table.patchmeta tr th, table.patchmeta tr td { + text-align: left; + padding: 3px 10px 3px 10px; + vertical-align: middle; +} + +.patchnav { + padding-left: 1em; + padding-top: 1em; +} + +.comment .meta { + background: #f0f0f0; +} + +.patch .content { + border: thin solid gray; + padding: 1em; +} + +.quote { + color: #007f00; +} + +span.p_header { color: #2e8b57; font-weight: bold; } +span.p_chunk { color: #a52a2a; font-weight: bold; } +span.p_context { color: #a020f0; } +span.p_add { color: #008b8b; } +span.p_del { color: #6a5acd; } +span.p_mod { color: #0000ff; } + +.acked-by { + color: red; + +} + +.signed-off-by { + color: red; + font-weight: bold; +} + +.from { + font-weight: bold; +} + +/* bundles */ +table.bundlelist { + border: thin solid black; +} + +table.bundlelist th { + padding-left: 2em; + padding-right: 2em; + background: #eeeeee; + border-bottom: thin solid black; +} + +table.bundlelist td +{ + padding-left: 2em; + padding-right: 2em; +} + +/* forms that appear for a patch */ +div.patchform { + border: thin solid gray; + padding-left: 0.6em; + padding-right: 0.6em; + float: left; + margin: 0.5em 1em; +} + +div.patchform h3 { + margin-top: 0em; + margin-left: -0.6em; + margin-right: -0.6em; + padding-left: 0.3em; + padding-right: 0.3em; + background: #786fb4; + color: white; + font-size: 100%; +} + +div.patchform ul { + list-style-type: none; + padding-left: 0.2em; + margin-top: 0em; +} + +/* forms */ +table.form { +} + +span.help_text { + font-size: 80%; +} + + +table.form td { + padding: 0.6em; + vertical-align: top; +} + +table.form th.headerrow { + background: #786fb4; + color: white; + font-weight: bold; + text-align: center; +} + +table.form th { + font-weight: normal; + text-align: left; + vertical-align: top; + padding-top: 0.6em; +} + +table.form td.form-help { + font-size: smaller; + padding-bottom: 1em; + padding-top: 0em; +} + +table.form tr td.submitrow { + border-bottom: 0.2em solid #786fb4; + text-align: center; +} + +table.registerform { + margin-left: auto; + margin-right: auto; +} +table.loginform { + margin-left: auto; + margin-right: auto; + width: 30em; +} + +/* form errors */ +.errorlist { + color: red; + list-style-type: none; + padding-left: 0.2em; + margin: 0em; +} + +/* generic table with header columns on the left */ +table.horizontal { + border-collapse: collapse; + border: thin solid #e8e8e8; +} + +table.horizontal th { + text-align: left; +} + +table.horizontal td, table.horizontal th { + padding: 0.5em 1em; + border: thin solid #e8e8e8; +} + +/* generic table with header row */ +table.vertical { + border-collapse: collapse; +} +table.vertical th { + background: #786fb4; + color: white; + font-weight: bold; + text-align: center; +} + +table.vertical th, table.vertical td { + padding: 0.2em 0.8em; + border: thin solid #e8e8e8; +} + +td.numberformat { + text-align: right; +} diff --git a/htdocs/images/filter-add.png b/htdocs/images/filter-add.png new file mode 100644 index 0000000000000000000000000000000000000000..3992342ddb7290ceb273771845297d67c75566b7 GIT binary patch literal 397 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvc1|%fwSC$-MnF?|JzX3_BrdZa z-0RJh$Z+7{{ktnO`JAGPodcZHW=5n~si`XeW1 zroL&*ys)r2?fQqxnjbcC-`x0gMHiUAQ2fQD!ocBuL6(DI%fk)P_akq}T6ZivS^s3> ztR*uvjBn0gc4Ni1L~eCyoz!eg?IOX6jh`F#WQwQ=9@o;2+up1F&m;5v(y*tw_EY9L nt@^Ys?$@EGHH$v|{ms8*W`=o8O~zkfSTT6I`njxgN@xNAG3K5x literal 0 HcmV?d00001 diff --git a/htdocs/images/filter-remove.png b/htdocs/images/filter-remove.png new file mode 100644 index 0000000000000000000000000000000000000000..82f2a328960eacfe0ee2658ae64b1c1141f1cd62 GIT binary patch literal 320 zcmeAS@N?(olHy`uVBq!ia0vp^93afW1|*O0@9PFqjKx9jP7LeL$-D$|*pj^6T^Rm@ z;DWu&Cj&(|3p^r=85p>QL70(Y)*K0-AbW|YuPgfvc1|%f@%LYIKxQ8Bba4!kxZK*W z>)(_p;dcJ7&G*;ZXV&`_o(=*_xQ-kL`h~&M L)z4*}Q$iB}p|yqB literal 0 HcmV?d00001 diff --git a/htdocs/images/title-background.png b/htdocs/images/title-background.png new file mode 100644 index 0000000000000000000000000000000000000000..d850ad7ea85abacf443b7937bcf23db3ab8c683e GIT binary patch literal 246 zcmeAS@N?(olHy`uVBq!ia0vp^j6iI}!3HGf{@&OH5-1LGcVbv~PUa<$!7|4$Z3 z_{`oB(j~Q<_1Idr+l%BDKkL>s`x%;K_*2W+PiKYrvPyC5X|lINUZ-q2Bg}nG`PS=- jqR2SMvRYjuHYS@-wM>5tSnl2g8qVP9>gTe~DWM4fJNry8 literal 0 HcmV?d00001 diff --git a/htdocs/js/autocomplete.js b/htdocs/js/autocomplete.js new file mode 100644 index 0000000..115ffba --- /dev/null +++ b/htdocs/js/autocomplete.js @@ -0,0 +1,43 @@ + + +function ac_keyup(input) +{ + input.autocomplete.keyup(); +} + +function AutoComplete(input) +{ + this.input = input; + this.div = null; + this.last_value = ''; + + input.autocomplete = this; + + this.hide = function() + { + if (this.div) { + this.div.style.display = 'none'; + this.div = null; + } + + } + + this.show = function() + { + if (!this.div) { + this.div = + + this.keyup = function() + { + value = input.value; + + if (value == this.last_value) + return; + + if (value.length < 3) { + this.hide(); + } + + +} + diff --git a/htdocs/js/filters.js b/htdocs/js/filters.js new file mode 100644 index 0000000..d8596ea --- /dev/null +++ b/htdocs/js/filters.js @@ -0,0 +1,78 @@ + +var available_filters = new Array(); + +function Filter(param, input_html, fn) +{ + this.param = param; + this.input_html = input_html; + this.fn = fn; +} + +function add_filter_change(input) +{ + index = input.selectedIndex - 1; + + if (index < 0 || index >= available_filters.length) + return; + + filter = available_filters[index]; + + value_element = document.getElementById("addfiltervalue"); + value_element.innerHTML = filter.input_html; +} + +function filter_form_submit(form) +{ + filter_index = form.filtertype.selectedIndex - 1; + + if (filter_index < 0 || filter_index >= available_filters.length) + return false; + + filter = available_filters[filter_index]; + + value = filter.fn(form); + updated = false; + + form = document.forms.filterparams; + + for (x = 0; x < form.elements.length; x++) { + if (form.elements[x].name == filter.param) { + form.elements[x].value = value; + updated = true; + } + } + + if (!updated && value) { + form.innerHTML = form.innerHTML + + ''; + } + + form.submit(); + + return false; +} + + +var submitter_input_prev_value = ''; + +function submitter_input_change(input) +{ + value = input.value; + + if (value.length < 3) + return; + + if (value == submitter_input_prev_value) + return; + + div = document.getElementById('submitter_complete'); + div.innerHTML = value; + div.style.display = 'block'; + div.style.position = 'relative'; + div.style.top = '4em'; + div.style.width = '15em'; + div.style.background = '#f0f0f0'; + div.style.padding = '0.2em'; + div.style.border = 'thin solid red'; +} diff --git a/htdocs/js/people.js b/htdocs/js/people.js new file mode 100644 index 0000000..7fb4e9f --- /dev/null +++ b/htdocs/js/people.js @@ -0,0 +1,5 @@ + +function personpopup(name) +{ + alert("meep!"); +} diff --git a/lib/apache2/patchwork.fastcgi.conf b/lib/apache2/patchwork.fastcgi.conf new file mode 100644 index 0000000..78d8147 --- /dev/null +++ b/lib/apache2/patchwork.fastcgi.conf @@ -0,0 +1,17 @@ +NameVirtualHost patchwork.example.com:80 + + DocumentRoot /srv/patchwork/htdocs/ + + Alias /media/ /srv/patchwork/lib/python/django/contrib/admin/media/ + + FastCGIExternalServer /srv/patchwork/htdocs/patchwork.fcgi -socket /srv/patchwork/var/fcgi.sock + + RewriteEngine On + RewriteCond %{REQUEST_URI} !^/(images|css|js|media)/.* + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^/(.*)$ /patchwork.fcgi/$1 [QSA,L] + + LogLevel warn + ErrorLog /var/log/apache2/patchwork-error.log + CustomLog /var/log/apache2/patchwork-acess.log combined + diff --git a/lib/apache2/patchwork.mod_python.conf b/lib/apache2/patchwork.mod_python.conf new file mode 100644 index 0000000..a84a9e2 --- /dev/null +++ b/lib/apache2/patchwork.mod_python.conf @@ -0,0 +1,22 @@ +NameVirtualHost patchwork.example.com:80 + + DocumentRoot /srv/patchwork/htdocs/ + + Alias /media/ /srv/patchwork/lib/python/django/contrib/admin/media/ + + + SetHandler python-program + PythonHandler django.core.handlers.modpython + PythonPath "['/srv/patchwork/apps', '/srv/patchwork/lib/python'] + sys.path" + SetEnv DJANGO_SETTINGS_MODULE patchwork.settings + + + + SetHandler None + + + LogLevel warn + ErrorLog /var/log/apache2/patchwork-error.log + CustomLog /var/log/apache2/patchwork-acess.log combined + + diff --git a/lib/sql/grant-all.sql b/lib/sql/grant-all.sql new file mode 100644 index 0000000..4b8a43b --- /dev/null +++ b/lib/sql/grant-all.sql @@ -0,0 +1,68 @@ +BEGIN; +-- give necessary permissions to the web server. Becuase the admin is all +-- web-based, these need to be quite permissive +GRANT SELECT, UPDATE, INSERT, DELETE ON + auth_message, + django_session, + django_site, + django_admin_log, + django_content_type, + auth_group_permissions, + auth_user, + auth_user_groups, + auth_group, + auth_user_user_permissions, + auth_permission, + patchwork_registrationrequest, + patchwork_userpersonconfirmation, + patchwork_state, + patchwork_comment, + patchwork_person, + patchwork_userprofile, + patchwork_userprofile_maintainer_projects, + patchwork_project, + patchwork_bundle, + patchwork_bundle_patches, + patchwork_patch +TO "www-data"; +GRANT SELECT, UPDATE ON + auth_group_id_seq, + auth_group_permissions_id_seq, + auth_message_id_seq, + auth_permission_id_seq, + auth_user_groups_id_seq, + auth_user_id_seq, + auth_user_user_permissions_id_seq, + django_admin_log_id_seq, + django_content_type_id_seq, + django_site_id_seq, + patchwork_bundle_id_seq, + patchwork_bundle_patches_id_seq, + patchwork_comment_id_seq, + patchwork_patch_id_seq, + patchwork_person_id_seq, + patchwork_project_id_seq, + patchwork_registrationrequest_id_seq, + patchwork_state_id_seq, + patchwork_userpersonconfirmation_id_seq, + patchwork_userprofile_id_seq, + patchwork_userprofile_maintainer_projects_id_seq +TO "www-data"; + +-- allow the mail user (in this case, 'nobody') to add patches +GRANT INSERT, SELECT ON + patchwork_patch, + patchwork_comment, + patchwork_person +TO "nobody"; +GRANT SELECT ON + patchwork_project +TO "nobody"; +GRANT UPDATE, SELECT ON + patchwork_patch_id_seq, + patchwork_person_id_seq, + patchwork_comment_id_seq +TO "nobody"; + +COMMIT; + diff --git a/templates/patchwork/base.html b/templates/patchwork/base.html new file mode 100644 index 0000000..c3a2206 --- /dev/null +++ b/templates/patchwork/base.html @@ -0,0 +1,77 @@ +{% load pwurl %} + + + + + {% block title %}Patchwork{% endblock %} - Patchwork + +{% block headers %}{% endblock %} + + +
+

+ Patchworkα + {% block heading %}{% endblock %}

+
+{% if user.is_authenticated %} + Logged in as + {{ user.username }} +
+ profile :: + todo + ({{ user.get_profile.n_todo_patches }})
+ logout :: + help +{% else %} + login +
+ register +
+ help +{% endif %} +
+
+
+ +{% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+{% endif %} +
+{% block body %} +{% endblock %} +
+ + + + + + diff --git a/templates/patchwork/bundle-public.html b/templates/patchwork/bundle-public.html new file mode 100644 index 0000000..0ee57da --- /dev/null +++ b/templates/patchwork/bundle-public.html @@ -0,0 +1,12 @@ +{% extends "patchwork/base.html" %} + +{% load person %} + +{% block title %}{{project.name}}{% endblock %} +{% block heading %}Bundle: {{bundle.name}}{% endblock %} + +{% block body %} + +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/templates/patchwork/bundle.html b/templates/patchwork/bundle.html new file mode 100644 index 0000000..8fa694a --- /dev/null +++ b/templates/patchwork/bundle.html @@ -0,0 +1,39 @@ +{% extends "patchwork/base.html" %} + +{% load person %} + +{% block title %}{{project.name}}{% endblock %} +{% block heading %}bundle: {{bundle.name}}{% endblock %} + +{% block body %} + +

This bundle contains patches for the {{ bundle.project.linkname }} +project.

+ +

Download bundle as mbox

+ + +
+ + + + + + + +{{ bundleform }} + + + +
Bundle settings
+ + +
+
+ +
+ +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/templates/patchwork/filters.html b/templates/patchwork/filters.html new file mode 100644 index 0000000..482bc98 --- /dev/null +++ b/templates/patchwork/filters.html @@ -0,0 +1,173 @@ + + + +
+
+ Filters: + {% if filters.applied_filters %} + {% for filter in filters.applied_filters %} + {{ filter.name }} = {{ filter.condition }} + {% if not filter.forced %} + + {% endif %} + {% if not forloop.last %}   |   {% endif %} + {% endfor %} + {% else %} + none + + {% endif %} +
+ +
+ + diff --git a/templates/patchwork/list.html b/templates/patchwork/list.html new file mode 100644 index 0000000..755c047 --- /dev/null +++ b/templates/patchwork/list.html @@ -0,0 +1,24 @@ +{% extends "patchwork/base.html" %} + +{% load person %} + +{% block title %}{{project.name}}{% endblock %} +{% block heading %}{{project.name}}{% endblock %} + +{% block body %} + +

Incoming patches

+ +{% if errors %} +

The following error{{ errors|length|pluralize:" was,s were" }} encountered +while updating patches:

+
    +{% for error in errors %} +
  • {{ error }}
  • +{% endfor %} +
+{% endif %} + +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/templates/patchwork/login.html b/templates/patchwork/login.html new file mode 100644 index 0000000..4706dda --- /dev/null +++ b/templates/patchwork/login.html @@ -0,0 +1,26 @@ +{% extends "patchwork/base.html" %} + +{% block title %}Patchwork Login{% endblock %} +{% block heading %}Patchwork Login{% endblock %} + + +{% block body %} +
+ + + + + {% if error %} + + + + {% endif %} + {{ form }} + + + +
login
{{ error }}
+ +
+
+{% endblock %} diff --git a/templates/patchwork/logout.html b/templates/patchwork/logout.html new file mode 100644 index 0000000..737f1ce --- /dev/null +++ b/templates/patchwork/logout.html @@ -0,0 +1,8 @@ +{% extends "patchwork/base.html" %} + +{% block title %}Patchwork{% endblock %} +{% block heading %}Patchwork{% endblock %} + +{% block body %} +

Logged out

+{% endblock %} diff --git a/templates/patchwork/pagination.html b/templates/patchwork/pagination.html new file mode 100644 index 0000000..3e95126 --- /dev/null +++ b/templates/patchwork/pagination.html @@ -0,0 +1,45 @@ +{% load listurl %} + +{% ifnotequal page.paginator.num_pages 1 %} +
+{% if page.has_previous %} + + « Previous +{% else %} + « Previous +{% endif %} + +{% if page.paginator.trailing_set %} + {% for p in page.paginator.trailing_set %} + {{ p }} + {% endfor %} + ... +{% endif %} + +{% for p in page.paginator.adjacent_set %} + {% ifequal p page.number %} + {{ p }} + {% else %} + {{ p }} + {% endifequal %} +{% endfor %} + +{% if page.paginator.leading_set %} + ... + {% for p in page.paginator.leading_set %} + {{ p }} + {% endfor %} +{% endif %} + +{% if page.has_next %} + + Next » + +{% else %} + Next » +{% endif %} +
+{% endifnotequal %} diff --git a/templates/patchwork/patch-form.html b/templates/patchwork/patch-form.html new file mode 100644 index 0000000..9d2c954 --- /dev/null +++ b/templates/patchwork/patch-form.html @@ -0,0 +1,87 @@ + +
+ +
+

Properties

+ + + + + + + + + + + + +
Change state:{{ patchform.state }}
Delegate to: + {{ patchform.delegate }}
+ +
+ +
+ +
+

Actions

+ + + + + + + + + +{% if bundles %} + + + + +{% endif %} + + + + +
Ack: +
+ + +
+
Create bundle: + {% if createbundleform.name.errors %} + {{createbundleform.errors}} + {% endif %} +
+ + {{ createbundleform.name }} + +
+
Add to bundle: +
+ + + + +
+
Archive: +
+ + +
+
+ + +
+ +
+
+
+ diff --git a/templates/patchwork/patch-list.html b/templates/patchwork/patch-list.html new file mode 100644 index 0000000..0a15e9c --- /dev/null +++ b/templates/patchwork/patch-list.html @@ -0,0 +1,185 @@ +{% load person %} +{% load listurl %} + +{% include "patchwork/pagination.html" %} + + + + + + +
+ {% include "patchwork/filters.html" %} +
+ +{% if page.paginator.long_page and user.is_authenticated %} +
+ +
+{% endif %} + +
+ + + + + {% if patchform or bundle %} + + + + + + + + + + +{% if page %} + {% for patch in page.object_list %} + + {% if patchform or bundle %} + + {% endif %} + + + + + + {% endfor %} +
+ {% endif %} + + + {% ifequal order.name "name" %} + Patch + {% else %} + Patch + {% endifequal %} + + {% ifequal order.name "date" %} + Date + {% else %} + Date + {% endifequal %} + + {% ifequal order.name "submitter" %} + Submiter + {% else %} + Submitter + {% endifequal %} + + {% ifequal order.name "state" %} + State + {% else %} + State + {% endifequal %} +
+ + {{ patch.name }}{{ patch.date|date:"Y-m-d" }}{{ patch.submitter|personify }}{{ patch.state }}
+ +{% include "patchwork/pagination.html" %} + +
+ +{% if patchform %} +
+

Properties

+ + + + + + + + + + + + + + + +
Change state: + {{ patchform.state }} + {{ patchform.state.errors }} +
Delegate to: + + {{ patchform.delegate }} + {{ patchform.delegate.errors }} +
Archive: + + {{ patchform.archived }} + {{ patchform.archived.errors }} +
+ +
+
+ +{% endif %} + +{% if user.is_authenticated %} +
+

Bundling

+ + + + + + + {% if bundles %} + + + + + {% endif %} + {% if bundle %} + + + + + {% endif %} +
Create bundle: + + +
Add to bundle: + + +
Remove from bundle: + + +
+
+{% endif %} + + +
+
+
+ +{% else %} + + No patches to display + +{% endif %} + + +
+ diff --git a/templates/patchwork/patch.html b/templates/patchwork/patch.html new file mode 100644 index 0000000..6ca6761 --- /dev/null +++ b/templates/patchwork/patch.html @@ -0,0 +1,214 @@ +{% extends "patchwork/base.html" %} + +{% load syntax %} +{% load person %} +{% load patch %} + +{% block title %}{{patch.name}} - Patchwork{% endblock %} +{% block heading %}{{patch.name}}{%endblock%} + +{% block body %} + + + + + + + + + + + + + + + + + + + + + + + + + +{% if patch.delegate %} + + + +{% endif %} + + + + +
Submitter{{ patch.submitter|personify }}
Date{{ patch.date }}
Message ID{{ patch.msgid }}
Download + mbox | + patch +
Permalink{{ patch.get_absolute_url }} +
State + {{ patch.state.name }}{% if patch.archived %}, archived{% endif %}
Delegated to: + {{ patch.delegate.get_profile.name }}
Headersshow + +
+ +
+ +{% if patchform %} +
+

Patch Properties

+
+ + + + + + + + + + + + + + + +
Change state: + {{ patchform.state }} + {{ patchform.state.errors }} +
Delegate to: + + {{ patchform.delegate }} + {{ patchform.delegate.errors }} +
Archived: + + {{ patchform.archived }} + {{ patchform.archived.errors }} +
+ +
+
+
+{% endif %} + +{% if createbundleform %} +
+

Bundling

+ + + + + + +{% if bundles %} + + + + +{% endif %} +
Create bundle: + {% if createbundleform.non_field_errors %} +
{{createbundleform.non_field_errors}}
+ {% endif %} +
+ + {% if createbundleform.name.errors %} +
{{createbundleform.name.errors}}
+ {% endif %} + {{ createbundleform.name }} + +
+
Add to bundle: +
+ + + +
+
+ + +
+{% endif %} + +{% if actionsform %} +
+

Actions

+ + + + + +
Ack: +
+ + +
+
+ +
+ +{% endif %} +
+
+
+ + + + +

Comments

+{% for comment in patch.comments %} +
+
{{ comment.submitter|personify }} - {{comment.date}}
+
+{{ comment|commentsyntax }}
+
+
+{% endfor %} + +

Patch

+
+
+{{ patch|patchsyntax }}
+
+
+ +{% endblock %} diff --git a/templates/patchwork/patchlist.html b/templates/patchwork/patchlist.html new file mode 100644 index 0000000..1bcd2c1 --- /dev/null +++ b/templates/patchwork/patchlist.html @@ -0,0 +1,36 @@ + +{% load person %} + +{% if patches %} +
+ + + {% if patchform %} + + + + + + {% for patch in patches %} + + {% if patchform %} + + {% endif %} + + + + + + {% endfor %} +
+ {% endif %} + PatchDateSubmitterState
+ + + {{ patch.name }}{{ patch.date|date:"Y-m-d" }}{{ patch.submitter|personify }}{{ patch.state }}
+ +{% include "patchwork/patch-form.html" %} + +{% else %} +

No patches to display

+{% endif %} diff --git a/templates/patchwork/profile.html b/templates/patchwork/profile.html new file mode 100644 index 0000000..35f3d4f --- /dev/null +++ b/templates/patchwork/profile.html @@ -0,0 +1,114 @@ +{% extends "patchwork/base.html" %} + +{% block title %}User Profile: {{ user.username }}{% endblock %} +{% block heading %}User Profile: {{ user.username }}{% endblock %} + + +{% block body %} + +

+{% if user.get_profile.maintainer_projects.count %} +Maintainer of +{% for project in user.get_profile.maintainer_projects.all %} +{{ project.linkname }}{% if not forloop.last %},{% endif %}{% endfor %}. +{% endif %} + +{% if user.get_profile.contributor_projects.count %} +Contributor to +{% for project in user.get_profile.contributor_projects.all %} +{{ project.linkname }}{% if not forloop.last %},{% endif %}{% endfor %}. +{% endif %} +

+ +

Todo

+{% if user.get_profile.n_todo_patches %} +

Your todo +list contains {{ user.get_profile.n_todo_patches }} +patch{{ user.get_profile.n_todo_patches|pluralize:"es" }}.

+{% else %} +

Your todo list contains patches that have been delegated to you. You +have no items in your todo list at present.

+{% endif %} +

Bundles

+ +{% if bundles %} + + + + + +{% for bundle in bundles %} + + + + + +{% endfor %} +
Bundle namePatches + Public Link
{{ bundle.name }}{{ bundle.n_patches }} + {% if bundle.public %} + {{ bundle.public_url }} + {% endif %} +
+{% else %} +

no bundles

+{% endif %} + + +

Linked email addresses

+

The following email addresses are associated with this patchwork account. +Adding alternative addresses allows patchwork to group contributions that +you have made under different addressses.

+

Adding a new email address will send a confirmation email to that +address.

+ + + + + + + + +{% for email in linked_emails %} + {% ifnotequal email.email user.email %} + + + + {% endifnotequal %} +{% endfor %} + + + +
email +
{{ user.email }}
{{ email.email }} + {% ifnotequal user.email email.email %} + + + + {% endifnotequal %} +
+
+ {{ linkform.email }} + +
+
+ +

Settings

+ +
+ +{{ profileform }} + + + +
+ + +
+
+ +{% endblock %} diff --git a/templates/patchwork/project.html b/templates/patchwork/project.html new file mode 100644 index 0000000..4ea1009 --- /dev/null +++ b/templates/patchwork/project.html @@ -0,0 +1,32 @@ +{% extends "patchwork/base.html" %} + +{% block title %}{{ project.name }}{% endblock %} +{% block heading %}{{ project.name }}{% endblock %} + +{% block body %} + + + + + + + + + + + + + + + + +
Name{{project.name}} +
List address{{project.listemail}}
Maintainer{{maintainers|length|pluralize}} + {% for maintainer in maintainers %} + {{ maintainer.get_profile.name }} + <{{maintainer.email}}> +
+ {% endfor %} +
Patch count{{n_patches}} (+ {{n_archived_patches}} archived)
+ +{% endblock %} diff --git a/templates/patchwork/projects.html b/templates/patchwork/projects.html new file mode 100644 index 0000000..349f314 --- /dev/null +++ b/templates/patchwork/projects.html @@ -0,0 +1,21 @@ +{% extends "patchwork/base.html" %} + +{% block title %}Project List{% endblock %} +{% block heading %}Project List{% endblock %} + +{% block body %} + +{% if projects %} +
+ {% for p in projects %} +
+ {{p.linkname}}
+
{{p.name}}
+ {% endfor %} +
+{% else %} +

Patchwork doesn't have any projects to display!

+{% endif %} + +{% endblock %} diff --git a/templates/patchwork/register-confirm.html b/templates/patchwork/register-confirm.html new file mode 100644 index 0000000..2af5744 --- /dev/null +++ b/templates/patchwork/register-confirm.html @@ -0,0 +1,13 @@ +{% extends "patchwork/base.html" %} + +{% block title %}Registration{% endblock %} +{% block heading %}Registration{% endblock %} + +{% block body %} +

Registraton confirmed!

+ +

Your patchwork registration is complete. Head over to your profile to start using +patchwork's extra features.

+ +{% endblock %} diff --git a/templates/patchwork/register.html b/templates/patchwork/register.html new file mode 100644 index 0000000..8bd422e --- /dev/null +++ b/templates/patchwork/register.html @@ -0,0 +1,122 @@ +{% extends "patchwork/base.html" %} + +{% block title %}Patchwork Registration{% endblock %} +{% block heading %}Patchwork Registration{% endblock %} + + +{% block body %} + +{% if request %} +

Registration successful!

+

email sent to {{ request.email }}

+

Beta note: While we're testing, the confirmation email has been replaced + by a single link: + {% url patchwork.views.user.register_confirm key=request.key %} +

+{% else %} +

By creating a patchwork account, you can:

+

    +
  • create "bundles" of patches
  • +
  • update the state of your own patches
  • +
+
+ + + + + {% if error %} + + + + {% endif %} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
register
{{ error }}
{{ form.first_name.label_tag }} +{% if form.first_name.errors %} + {{ form.first_name.errors }} +{% endif %} + {{ form.first_name }} +{% if form.first_name.help_text %} +
{{ form.first_name.help_text }}
+{% endif %} +
{{ form.last_name.label_tag }} +{% if form.last_name.errors %} + {{ form.last_name.errors }} +{% endif %} + {{ form.last_name }} +{% if form.last_name.help_text %} +
{{ form.last_name.help_text }}
+{% endif %} +
+ Your name is used to identify you on the site +
{{ form.email.label_tag }} +{% if form.email.errors %} + {{ form.email.errors }} +{% endif %} + {{ form.email }} +{% if form.email.help_text %} +
{{ form.email.help_text }}
+{% endif %} +
+ Patchwork will send a confirmation email to this address +
{{ form.username.label_tag }} +{% if form.username.errors %} + {{ form.username.errors }} +{% endif %} + {{ form.username }} +{% if form.username.help_text %} +
{{ form.username.help_text }}
+{% endif %} +
{{ form.password.label_tag }} +{% if form.password.errors %} + {{ form.password.errors }} +{% endif %} + {{ form.password }} +{% if form.password.help_text %} +
{{ form.password.help_text }}
+{% endif %} +
+ +
+
+{% endif %} + +{% endblock %} diff --git a/templates/patchwork/todo-list.html b/templates/patchwork/todo-list.html new file mode 100644 index 0000000..8a5ab7a --- /dev/null +++ b/templates/patchwork/todo-list.html @@ -0,0 +1,17 @@ +{% extends "patchwork/base.html" %} + +{% load person %} + +{% block title %}{{ user }}'s todo list{% endblock %} +{% block heading %}{{user}}'s todo list for {{ project.linkname }}{% endblock %} + +{% block body %} + +

A Patchwork Todo-list contains patches that are assigned to you, and +are in an "action required" state +({% for state in action_required_states %}{% if forloop.last and not forloop.first %} or {% endif %}{{ state }}{% if not forloop.last and not forloop.first %}, {%endif %}{% endfor %}), and are not archived. +

+ +{% include "patchwork/patch-list.html" %} + +{% endblock %} diff --git a/templates/patchwork/todo-lists.html b/templates/patchwork/todo-lists.html new file mode 100644 index 0000000..8eb10cc --- /dev/null +++ b/templates/patchwork/todo-lists.html @@ -0,0 +1,29 @@ +{% extends "patchwork/base.html" %} + +{% block title %}{{ user }}'s todo lists{% endblock %} +{% block heading %}{{ user }}'s todo lists{% endblock %} + +{% block body %} + +{% if todo_lists %} +

You have multiple todo lists. Each todo list contains patches for a single + project.

+ + + + + +{% for todo_list in todo_lists %} + + + + +{% endfor %} +
projectpatches
{{ todo_list.project.name }}{{ todo_list.n_patches }}
+ +{% else %} + No todo lists +{% endif %} +{% endblock %} diff --git a/templates/patchwork/user-link-confirm.html b/templates/patchwork/user-link-confirm.html new file mode 100644 index 0000000..61979cf --- /dev/null +++ b/templates/patchwork/user-link-confirm.html @@ -0,0 +1,19 @@ +{% extends "patchwork/base.html" %} + +{% block title %}{{ user.username }}{% endblock %} +{% block heading %}link accounts for {{ user.username }}{% endblock %} + + +{% block body %} + +{% if errors %} +

{{ errors }}

+{% else %} +

You have sucessfully linked the email address {{ person.email }} to + your patchwork account

+ +{% endif %} +

Back to your + profile.

+ +{% endblock %} diff --git a/templates/patchwork/user-link.html b/templates/patchwork/user-link.html new file mode 100644 index 0000000..3eeb527 --- /dev/null +++ b/templates/patchwork/user-link.html @@ -0,0 +1,30 @@ +{% extends "patchwork/base.html" %} + +{% block title %}{{ user.username }}{% endblock %} +{% block heading %}link accounts for {{ user.username }}{% endblock %} + + +{% block body %} + +{% if confirmation %} +

A confirmation email has been sent to {{ confirmation.email }}.

+ +

beta link: {% url patchwork.views.user.link_confirm key=confirmation.key %}

+ +{% else %} + + {% if form.errors %} +

There was an error submitting your link request.

+ {{ form.non_field_errors }} + {% endif %} + +
+ {{linkform.email.errors}} + Link an email address: {{ linkform.email }} +
+ +{% endif %} + +{% endblock %} -- 2.39.2