diff options
author | Jeremy Kerr <jk@ozlabs.org> | 2015-05-24 16:57:33 +0800 |
---|---|---|
committer | Jeremy Kerr <jk@ozlabs.org> | 2015-05-27 10:26:41 +0800 |
commit | ad2762cf775a8dde508de47164d6429f3fd724f1 (patch) | |
tree | e63015a468cfe32c961908f0338d423227799815 /patchwork/views | |
parent | f09e982f58384946111d4157fd2b7c2b31b78612 (diff) | |
download | patchwork-ad2762cf775a8dde508de47164d6429f3fd724f1.tar.bz2 patchwork-ad2762cf775a8dde508de47164d6429f3fd724f1.tar.xz |
Move to a more recent django project structure
This change updates patchwor to the newer project struture: we've moved
the actual application out of the apps/ directory, and the
patchwork-specific templates to under the patchwork application.
This gives us the manage.py script in the top-level now.
Signed-off-by: Jeremy Kerr <jk@ozlabs.org>
Diffstat (limited to 'patchwork/views')
-rw-r--r-- | patchwork/views/__init__.py | 220 | ||||
-rw-r--r-- | patchwork/views/base.py | 122 | ||||
-rw-r--r-- | patchwork/views/bundle.py | 221 | ||||
-rw-r--r-- | patchwork/views/mail.py | 119 | ||||
-rw-r--r-- | patchwork/views/patch.py | 107 | ||||
-rw-r--r-- | patchwork/views/project.py | 38 | ||||
-rw-r--r-- | patchwork/views/user.py | 216 | ||||
-rw-r--r-- | patchwork/views/xmlrpc.py | 450 |
8 files changed, 1493 insertions, 0 deletions
diff --git a/patchwork/views/__init__.py b/patchwork/views/__init__.py new file mode 100644 index 0000000..dfca56d --- /dev/null +++ b/patchwork/views/__init__.py @@ -0,0 +1,220 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from base import * +from patchwork.utils import Order, get_patch_ids, bundle_actions, set_bundle +from patchwork.paginator import Paginator +from patchwork.forms import MultiplePatchForm +from patchwork.models import Comment +import re +import datetime + +try: + from email.mime.nonmultipart import MIMENonMultipart + from email.encoders import encode_7or8bit + from email.parser import HeaderParser + from email.header import Header + import email.utils +except ImportError: + # Python 2.4 compatibility + from email.MIMENonMultipart import MIMENonMultipart + from email.Encoders import encode_7or8bit + from email.Parser import HeaderParser + from email.Header import Header + import email.Utils + email.utils = email.Utils + +def generic_list(request, project, view, + view_args = {}, filter_settings = [], patches = None, + editable_order = False): + + context = PatchworkRequestContext(request, + list_view = view, + list_view_params = view_args) + + context.project = project + order = Order(request.REQUEST.get('order'), editable = editable_order) + + # Explicitly set data to None because request.POST will be an empty dict + # when the form is not submitted, but passing a non-None data argument to + # a forms.Form will make it bound and we don't want that to happen unless + # there's been a form submission. + data = None + if request.method == 'POST': + data = request.POST + user = request.user + properties_form = None + if project.is_editable(user): + + # we only pass the post data to the MultiplePatchForm if that was + # the actual form submitted + data_tmp = None + if data and data.get('form', '') == 'patchlistform': + data_tmp = data + + properties_form = MultiplePatchForm(project, data = data_tmp) + + if request.method == 'POST' and data.get('form') == 'patchlistform': + action = data.get('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 data.get('bundle_name', False): + action = 'create' + + ps = Patch.objects.filter(id__in = get_patch_ids(data)) + + if action in bundle_actions: + errors = set_bundle(user, project, action, data, ps, context) + + elif properties_form and action == properties_form.action: + errors = process_multiplepatch_form(properties_form, user, + action, ps, context) + else: + errors = [] + + if errors: + context['errors'] = errors + + 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 patches is None: + patches = Patch.objects.filter(project=project) + + patches = context.filters.apply(patches) + if not editable_order: + patches = order.apply(patches) + + # we don't need the content or headers for a list; they're text fields + # that can potentially contain a lot of data + patches = patches.defer('content', 'headers') + + # but we will need to follow the state and submitter relations for + # rendering the list template + patches = patches.select_related('state', 'submitter') + + paginator = Paginator(request, patches) + + context.update({ + 'page': paginator.current_page, + 'patchform': properties_form, + 'project': project, + 'order': order, + }) + + return context + + +def process_multiplepatch_form(form, user, action, patches, context): + errors = [] + if not form.is_valid() or action != form.action: + return ['The submitted form data was invalid'] + + if len(patches) == 0: + context.add_message("No patches selected; nothing updated") + return errors + + changed_patches = 0 + for patch in patches: + if not patch.is_editable(user): + errors.append("You don't have permissions to edit patch '%s'" + % patch.name) + continue + + changed_patches += 1 + form.save(patch) + + if changed_patches == 1: + context.add_message("1 patch updated") + elif changed_patches > 1: + context.add_message("%d patches updated" % changed_patches) + else: + context.add_message("No patches updated") + + return errors + +class PatchMbox(MIMENonMultipart): + patch_charset = 'utf-8' + def __init__(self, _text): + MIMENonMultipart.__init__(self, 'text', 'plain', + **{'charset': self.patch_charset}) + self.set_payload(_text.encode(self.patch_charset)) + encode_7or8bit(self) + +def patch_to_mbox(patch): + postscript_re = re.compile('\n-{2,3} ?\n') + + comment = None + try: + comment = Comment.objects.get(patch = patch, msgid = patch.msgid) + except Exception: + pass + + body = '' + if comment: + body = comment.content.strip() + "\n" + + parts = postscript_re.split(body, 1) + if len(parts) == 2: + (body, postscript) = parts + body = body.strip() + "\n" + postscript = postscript.rstrip() + else: + postscript = '' + + for comment in Comment.objects.filter(patch = patch) \ + .exclude(msgid = patch.msgid): + body += comment.patch_responses() + + if postscript: + body += '---\n' + postscript + '\n' + + if patch.content: + body += '\n' + patch.content + + delta = patch.date - datetime.datetime.utcfromtimestamp(0) + utc_timestamp = delta.seconds + delta.days*24*3600 + + mail = PatchMbox(body) + mail['Subject'] = patch.name + mail['From'] = email.utils.formataddr(( + str(Header(patch.submitter.name, mail.patch_charset)), + patch.submitter.email)) + mail['X-Patchwork-Id'] = str(patch.id) + mail['Message-Id'] = patch.msgid + mail.set_unixfrom('From patchwork ' + patch.date.ctime()) + + + copied_headers = ['To', 'Cc', 'Date'] + orig_headers = HeaderParser().parsestr(str(patch.headers)) + for header in copied_headers: + if header in orig_headers: + mail[header] = orig_headers[header] + + if 'Date' not in mail: + mail['Date'] = email.utils.formatdate(utc_timestamp) + + return mail diff --git a/patchwork/views/base.py b/patchwork/views/base.py new file mode 100644 index 0000000..6d7dd13 --- /dev/null +++ b/patchwork/views/base.py @@ -0,0 +1,122 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project, Person, EmailConfirmation +from django.shortcuts import render_to_response, get_object_or_404 +from django.http import HttpResponse, HttpResponseRedirect, Http404 +from patchwork.requestcontext import PatchworkRequestContext +from django.core import serializers, urlresolvers +from django.template.loader import render_to_string +from django.conf import settings +from django.db.models import Q + +def projects(request): + context = PatchworkRequestContext(request) + projects = Project.objects.all() + + if projects.count() == 1: + return HttpResponseRedirect( + urlresolvers.reverse('patchwork.views.patch.list', + kwargs = {'project_id': projects[0].linkname})) + + context['projects'] = projects + return render_to_response('patchwork/projects.html', context) + +def pwclientrc(request, project_id): + project = get_object_or_404(Project, linkname = project_id) + context = PatchworkRequestContext(request) + context.project = project + if settings.FORCE_HTTPS_LINKS or request.is_secure(): + context['scheme'] = 'https' + else: + context['scheme'] = 'http' + response = HttpResponse(content_type = "text/plain") + response['Content-Disposition'] = 'attachment; filename=.pwclientrc' + response.write(render_to_string('patchwork/pwclientrc', context)) + return response + +def pwclient(request): + context = PatchworkRequestContext(request) + response = HttpResponse(content_type = "text/x-python") + response['Content-Disposition'] = 'attachment; filename=pwclient' + response.write(render_to_string('patchwork/pwclient', context)) + return response + +def confirm(request, key): + import patchwork.views.user, patchwork.views.mail + views = { + 'userperson': patchwork.views.user.link_confirm, + 'registration': patchwork.views.user.register_confirm, + 'optout': patchwork.views.mail.optout_confirm, + 'optin': patchwork.views.mail.optin_confirm, + } + + conf = get_object_or_404(EmailConfirmation, key = key) + if conf.type not in views: + raise Http404 + + if conf.active and conf.is_valid(): + return views[conf.type](request, conf) + + context = PatchworkRequestContext(request) + context['conf'] = conf + if not conf.active: + context['error'] = 'inactive' + elif not conf.is_valid(): + context['error'] = 'expired' + + return render_to_response('patchwork/confirm-error.html', context) + +def submitter_complete(request): + search = request.GET.get('q', '') + limit = request.GET.get('l', None) + response = HttpResponse(content_type = "text/plain") + + if len(search) <= 3: + return response + + queryset = Person.objects.filter(Q(name__icontains = search) | + Q(email__icontains = search)) + if limit is not None: + try: + limit = int(limit) + except ValueError: + limit = None + + if limit is not None and limit > 0: + queryset = queryset[:limit] + + json_serializer = serializers.get_serializer("json")() + json_serializer.serialize(queryset, ensure_ascii=False, stream=response) + return response + +help_pages = {'': 'index.html', + 'about/': 'about.html', + } + +if settings.ENABLE_XMLRPC: + help_pages['pwclient/'] = 'pwclient.html' + +def help(request, path): + context = PatchworkRequestContext(request) + if path in help_pages: + return render_to_response('patchwork/help/' + help_pages[path], context) + raise Http404 + diff --git a/patchwork/views/bundle.py b/patchwork/views/bundle.py new file mode 100644 index 0000000..3fb47e2 --- /dev/null +++ b/patchwork/views/bundle.py @@ -0,0 +1,221 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from django.contrib.auth.decorators import login_required +from django.contrib.auth.models import User +from django.shortcuts import render_to_response, get_object_or_404 +from patchwork.requestcontext import PatchworkRequestContext +from django.http import HttpResponse, HttpResponseRedirect, HttpResponseNotFound +import django.core.urlresolvers +from patchwork.models import Patch, Bundle, BundlePatch, Project +from patchwork.utils import get_patch_ids +from patchwork.forms import BundleForm, DeleteBundleForm +from patchwork.views import generic_list, patch_to_mbox +from patchwork.filters import DelegateFilter + +@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) + try: + bundle.append_patch(patch) + except Exception: + pass + 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.append_patch(patch) + except: + 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 bundles(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form_name = request.POST.get('form_name', '') + + if form_name == DeleteBundleForm.name: + form = DeleteBundleForm(request.POST) + if form.is_valid(): + bundle = get_object_or_404(Bundle, + id = form.cleaned_data['bundle_id']) + bundle.delete() + + bundles = Bundle.objects.filter(owner = request.user) + for bundle in bundles: + bundle.delete_form = DeleteBundleForm(auto_id = False, + initial = {'bundle_id': bundle.id}) + + context['bundles'] = bundles + + return render_to_response('patchwork/bundles.html', context) + +def bundle(request, username, bundlename): + bundle = get_object_or_404(Bundle, owner__username = username, + name = bundlename) + filter_settings = [(DelegateFilter, DelegateFilter.AnyDelegate)] + + is_owner = request.user == bundle.owner + + if not (is_owner or bundle.public): + return HttpResponseNotFound() + + if is_owner: + 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() + + # if we've changed the bundle name, redirect to new URL + bundle = Bundle.objects.get(pk = bundle.pk) + if bundle.name != bundlename: + return HttpResponseRedirect(bundle.get_absolute_url()) + + else: + form = BundleForm(instance = bundle) + else: + form = BundleForm(instance = bundle) + + if request.method == 'POST' and \ + request.POST.get('form') == 'reorderform': + order = get_object_or_404(BundlePatch, bundle = bundle, + patch__id = request.POST.get('order_start')).order + + for patch_id in request.POST.getlist('neworder'): + bundlepatch = get_object_or_404(BundlePatch, + bundle = bundle, patch__id = patch_id) + bundlepatch.order = order + bundlepatch.save() + order += 1 + else: + form = None + + context = generic_list(request, bundle.project, + 'patchwork.views.bundle.bundle', + view_args = {'username': bundle.owner.username, + 'bundlename': bundle.name}, + filter_settings = filter_settings, + patches = bundle.ordered_patches(), + editable_order = is_owner) + + context['bundle'] = bundle + context['bundleform'] = form + + return render_to_response('patchwork/bundle.html', context) + +def mbox(request, username, bundlename): + bundle = get_object_or_404(Bundle, owner__username = username, + name = bundlename) + + if not (request.user == bundle.owner or bundle.public): + return HttpResponseNotFound() + + mbox = '\n'.join([patch_to_mbox(p).as_string(True) + for p in bundle.ordered_patches()]) + + response = HttpResponse(content_type='text/plain') + response['Content-Disposition'] = \ + 'attachment; filename=bundle-%d-%s.mbox' % (bundle.id, bundle.name) + + response.write(mbox) + return response + +@login_required +def bundle_redir(request, bundle_id): + bundle = get_object_or_404(Bundle, id = bundle_id, owner = request.user) + return HttpResponseRedirect(bundle.get_absolute_url()) + +@login_required +def mbox_redir(request, bundle_id): + bundle = get_object_or_404(Bundle, id = bundle_id, owner = request.user) + return HttpResponseRedirect(django.core.urlresolvers.reverse( + 'patchwork.views.bundle.mbox', kwargs = { + 'username': request.user.username, + 'bundlename': bundle.name, + })) + + + diff --git a/patchwork/views/mail.py b/patchwork/views/mail.py new file mode 100644 index 0000000..aebba34 --- /dev/null +++ b/patchwork/views/mail.py @@ -0,0 +1,119 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2010 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +from patchwork.requestcontext import PatchworkRequestContext +from patchwork.models import EmailOptout, EmailConfirmation +from patchwork.forms import OptinoutRequestForm, EmailForm +from django.shortcuts import render_to_response +from django.template.loader import render_to_string +from django.conf import settings as conf_settings +from django.core.mail import send_mail +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect + +def settings(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = EmailForm(data = request.POST) + if form.is_valid(): + email = form.cleaned_data['email'] + is_optout = EmailOptout.objects.filter(email = email).count() > 0 + context.update({ + 'email': email, + 'is_optout': is_optout, + }) + return render_to_response('patchwork/mail-settings.html', context) + + else: + form = EmailForm() + context['form'] = form + return render_to_response('patchwork/mail-form.html', context) + +def optout_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + # silently ignore duplicated optouts + if EmailOptout.objects.filter(email = email).count() == 0: + optout = EmailOptout(email = email) + optout.save() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optout.html', context) + +def optin_confirm(request, conf): + context = PatchworkRequestContext(request) + + email = conf.email.strip().lower() + EmailOptout.objects.filter(email = email).delete() + + conf.deactivate() + context['email'] = conf.email + + return render_to_response('patchwork/optin.html', context) + +def optinout(request, action, description): + context = PatchworkRequestContext(request) + + mail_template = 'patchwork/%s-request.mail' % action + html_template = 'patchwork/%s-request.html' % action + + if request.method != 'POST': + return HttpResponseRedirect(reverse(settings)) + + form = OptinoutRequestForm(data = request.POST) + if not form.is_valid(): + context['error'] = ('There was an error in the %s form. ' + + 'Please review the form and re-submit.') % \ + description + context['form'] = form + return render_to_response(html_template, context) + + email = form.cleaned_data['email'] + if action == 'optin' and \ + EmailOptout.objects.filter(email = email).count() == 0: + context['error'] = ('The email address %s is not on the ' + + 'patchwork opt-out list, so you don\'t ' + + 'need to opt back in') % email + context['form'] = form + return render_to_response(html_template, context) + + conf = EmailConfirmation(type = action, email = email) + conf.save() + context['confirmation'] = conf + mail = render_to_string(mail_template, context) + try: + send_mail('Patchwork %s confirmation' % description, mail, + conf_settings.DEFAULT_FROM_EMAIL, [email]) + context['email'] = mail + context['email_sent'] = True + except Exception, ex: + context['error'] = 'An error occurred during confirmation . ' + \ + 'Please try again later.' + context['admins'] = conf_settings.ADMINS + + return render_to_response(html_template, context) + +def optout(request): + return optinout(request, 'optout', 'opt-out') + +def optin(request): + return optinout(request, 'optin', 'opt-in') diff --git a/patchwork/views/patch.py b/patchwork/views/patch.py new file mode 100644 index 0000000..62ff853 --- /dev/null +++ b/patchwork/views/patch.py @@ -0,0 +1,107 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project, Bundle +from patchwork.forms import PatchForm, CreateBundleForm +from patchwork.requestcontext import PatchworkRequestContext +from django.shortcuts import render_to_response, get_object_or_404 +from django.http import HttpResponse, HttpResponseForbidden +from patchwork.views import generic_list, patch_to_mbox + +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) + + 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.append_patch(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')) + try: + bundle.append_patch(patch) + bundle.save() + context.add_message('Patch added to bundle "%s"' % bundle.name) + except Exception, ex: + context.add_message("Couldn't add patch '%s' to bundle %s: %s" \ + % (patch.name, bundle.name, ex.message)) + + # 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') + + 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(content_type="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(content_type="text/plain") + response.write(patch_to_mbox(patch).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) diff --git a/patchwork/views/project.py b/patchwork/views/project.py new file mode 100644 index 0000000..114dbe0 --- /dev/null +++ b/patchwork/views/project.py @@ -0,0 +1,38 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2009 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from patchwork.models import Patch, Project +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib.auth.models import User +from patchwork.requestcontext import PatchworkRequestContext + +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( \ + profile__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) diff --git a/patchwork/views/user.py b/patchwork/views/user.py new file mode 100644 index 0000000..126ecc9 --- /dev/null +++ b/patchwork/views/user.py @@ -0,0 +1,216 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +from django.contrib.auth.decorators import login_required +from patchwork.requestcontext import PatchworkRequestContext +from django.shortcuts import render_to_response, get_object_or_404 +from django.contrib import auth +from django.contrib.sites.models import Site +from django.http import HttpResponseRedirect +from patchwork.models import Project, Bundle, Person, EmailConfirmation, \ + State, EmailOptout +from patchwork.forms import UserProfileForm, UserPersonLinkForm, \ + RegistrationForm +from patchwork.filters import DelegateFilter +from patchwork.views import generic_list +from django.template.loader import render_to_string +from django.conf import settings +from django.core.mail import send_mail +import django.core.urlresolvers + +def register(request): + context = PatchworkRequestContext(request) + if request.method == 'POST': + form = RegistrationForm(request.POST) + if form.is_valid(): + data = form.cleaned_data + # create inactive user + user = auth.models.User.objects.create_user(data['username'], + data['email'], + data['password']) + user.is_active = False; + user.first_name = data.get('first_name', '') + user.last_name = data.get('last_name', '') + user.save() + + # create confirmation + conf = EmailConfirmation(type = 'registration', user = user, + email = user.email) + conf.save() + + # send email + mail_ctx = {'site': Site.objects.get_current(), + 'confirmation': conf} + + subject = render_to_string('patchwork/activation_email_subject.txt', + mail_ctx).replace('\n', ' ').strip() + + message = render_to_string('patchwork/activation_email.txt', + mail_ctx) + + send_mail(subject, message, settings.DEFAULT_FROM_EMAIL, + [conf.email]) + + # setting 'confirmation' in the template indicates success + context['confirmation'] = conf + + else: + form = RegistrationForm() + + return render_to_response('patchwork/registration_form.html', + { 'form': form }, + context_instance=context) + +def register_confirm(request, conf): + conf.user.is_active = True + conf.user.save() + conf.deactivate() + try: + person = Person.objects.get(email__iexact = conf.user.email) + except Person.DoesNotExist: + person = Person(email = conf.user.email, + name = conf.user.profile.name()) + person.user = conf.user + person.save() + + return render_to_response('patchwork/registration-confirm.html') + +@login_required +def profile(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form = UserProfileForm(instance = request.user.profile, + data = request.POST) + if form.is_valid(): + form.save() + else: + form = UserProfileForm(instance = request.user.profile) + + context.project = request.user.profile.primary_project + context['bundles'] = Bundle.objects.filter(owner = request.user) + context['profileform'] = form + + optout_query = '%s.%s IN (SELECT %s FROM %s)' % ( + Person._meta.db_table, + Person._meta.get_field('email').column, + EmailOptout._meta.get_field('email').column, + EmailOptout._meta.db_table) + people = Person.objects.filter(user = request.user) \ + .extra(select = {'is_optout': optout_query}) + context['linked_emails'] = people + context['linkform'] = UserPersonLinkForm() + + return render_to_response('patchwork/profile.html', context) + +@login_required +def link(request): + context = PatchworkRequestContext(request) + + if request.method == 'POST': + form = UserPersonLinkForm(request.POST) + if form.is_valid(): + conf = EmailConfirmation(type = 'userperson', + user = request.user, + email = form.cleaned_data['email']) + conf.save() + context['confirmation'] = conf + + try: + send_mail('Patchwork email address confirmation', + render_to_string('patchwork/user-link.mail', + context), + settings.DEFAULT_FROM_EMAIL, + [form.cleaned_data['email']]) + except Exception: + context['confirmation'] = None + context['error'] = 'An error occurred during confirmation. ' + \ + 'Please try again later' + else: + form = UserPersonLinkForm() + context['linkform'] = form + + return render_to_response('patchwork/user-link.html', context) + +@login_required +def link_confirm(request, conf): + context = PatchworkRequestContext(request) + + try: + person = Person.objects.get(email__iexact = conf.email) + except Person.DoesNotExist: + person = Person(email = conf.email) + + person.link_to_user(conf.user) + person.save() + conf.deactivate() + + context['person'] = person + + return render_to_response('patchwork/user-link-confirm.html', context) + +@login_required +def unlink(request, person_id): + 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.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.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.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/patchwork/views/xmlrpc.py b/patchwork/views/xmlrpc.py new file mode 100644 index 0000000..84ed408 --- /dev/null +++ b/patchwork/views/xmlrpc.py @@ -0,0 +1,450 @@ +# Patchwork - automated patch tracking system +# Copyright (C) 2008 Jeremy Kerr <jk@ozlabs.org> +# +# This file is part of the Patchwork package. +# +# Patchwork is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# Patchwork is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Patchwork; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Patchwork XMLRPC interface +# + +from SimpleXMLRPCServer import SimpleXMLRPCDispatcher +from django.http import HttpResponse, HttpResponseRedirect, \ + HttpResponseServerError +from django.core import urlresolvers +from django.contrib.auth import authenticate +from patchwork.models import Patch, Project, Person, State +from patchwork.views import patch_to_mbox +from django.views.decorators.csrf import csrf_exempt + +import sys +import base64 +import xmlrpclib + +class PatchworkXMLRPCDispatcher(SimpleXMLRPCDispatcher): + def __init__(self): + if sys.version_info[:3] >= (2,5,): + SimpleXMLRPCDispatcher.__init__(self, allow_none=False, + encoding=None) + def _dumps(obj, *args, **kwargs): + kwargs['allow_none'] = self.allow_none + kwargs['encoding'] = self.encoding + return xmlrpclib.dumps(obj, *args, **kwargs) + else: + def _dumps(obj, *args, **kwargs): + return xmlrpclib.dumps(obj, *args, **kwargs) + SimpleXMLRPCDispatcher.__init__(self) + + self.dumps = _dumps + + # map of name => (auth, func) + self.func_map = {} + + def register_function(self, fn, auth_required): + self.func_map[fn.__name__] = (auth_required, fn) + + + def _user_for_request(self, request): + auth_header = None + + if 'HTTP_AUTHORIZATION' in request.META: + auth_header = request.META.get('HTTP_AUTHORIZATION') + elif 'Authorization' in request.META: + auth_header = request.META.get('Authorization') + + if auth_header is None or auth_header == '': + raise Exception("No authentication credentials given") + + str = auth_header.strip() + + if not str.startswith('Basic '): + raise Exception("Authentication scheme not supported") + + str = str[len('Basic '):].strip() + + try: + decoded = base64.decodestring(str) + username, password = decoded.split(':', 1) + except: + raise Exception("Invalid authentication credentials") + + return authenticate(username = username, password = password) + + + def _dispatch(self, request, method, params): + if method not in self.func_map.keys(): + raise Exception('method "%s" is not supported' % method) + + auth_required, fn = self.func_map[method] + + if auth_required: + user = self._user_for_request(request) + if not user: + raise Exception("Invalid username/password") + + params = (user,) + params + + return fn(*params) + + def _marshaled_dispatch(self, request): + try: + params, method = xmlrpclib.loads(request.body) + + response = self._dispatch(request, method, params) + # wrap response in a singleton tuple + response = (response,) + response = self.dumps(response, methodresponse=1) + except xmlrpclib.Fault, fault: + response = self.dumps(fault) + except: + # report exception back to server + response = self.dumps( + xmlrpclib.Fault(1, "%s:%s" % (sys.exc_type, sys.exc_value)), + ) + + return response + +dispatcher = PatchworkXMLRPCDispatcher() + +# XMLRPC view function +@csrf_exempt +def xmlrpc(request): + if request.method != 'POST': + return HttpResponseRedirect( + urlresolvers.reverse('patchwork.views.help', + kwargs = {'path': 'pwclient/'})) + + response = HttpResponse() + try: + ret = dispatcher._marshaled_dispatch(request) + response.write(ret) + except Exception: + return HttpResponseServerError() + + return response + +# decorator for XMLRPC methods. Setting login_required to true will call +# the decorated function with a non-optional user as the first argument. +def xmlrpc_method(login_required = False): + def wrap(f): + dispatcher.register_function(f, login_required) + return f + + return wrap + + + +# We allow most of the Django field lookup types for remote queries +LOOKUP_TYPES = ["iexact", "contains", "icontains", "gt", "gte", "lt", + "in", "startswith", "istartswith", "endswith", + "iendswith", "range", "year", "month", "day", "isnull" ] + +####################################################################### +# Helper functions +####################################################################### + +def project_to_dict(obj): + """Return a trimmed down dictionary representation of a Project + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'linkname' : obj.linkname, + 'name' : obj.name, + } + +def person_to_dict(obj): + """Return a trimmed down dictionary representation of a Person + object which is OK to send to the client.""" + + # Make sure we don't return None even if the user submitted a patch + # with no real name. XMLRPC can't marshall None. + if obj.name is not None: + name = obj.name + else: + name = obj.email + + return \ + { + 'id' : obj.id, + 'email' : obj.email, + 'name' : name, + 'user' : unicode(obj.user).encode("utf-8"), + } + +def patch_to_dict(obj): + """Return a trimmed down dictionary representation of a Patch + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'date' : unicode(obj.date).encode("utf-8"), + 'filename' : obj.filename(), + 'msgid' : obj.msgid, + 'name' : obj.name, + 'project' : unicode(obj.project).encode("utf-8"), + 'project_id' : obj.project_id, + 'state' : unicode(obj.state).encode("utf-8"), + 'state_id' : obj.state_id, + 'archived' : obj.archived, + 'submitter' : unicode(obj.submitter).encode("utf-8"), + 'submitter_id' : obj.submitter_id, + 'delegate' : unicode(obj.delegate).encode("utf-8"), + 'delegate_id' : max(obj.delegate_id, 0), + 'commit_ref' : max(obj.commit_ref, ''), + } + +def bundle_to_dict(obj): + """Return a trimmed down dictionary representation of a Bundle + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + 'n_patches' : obj.n_patches(), + 'public_url' : obj.public_url(), + } + +def state_to_dict(obj): + """Return a trimmed down dictionary representation of a State + object which is OK to send to the client.""" + return \ + { + 'id' : obj.id, + 'name' : obj.name, + } + +####################################################################### +# Public XML-RPC methods +####################################################################### + +@xmlrpc_method(False) +def pw_rpc_version(): + """Return Patchwork XML-RPC interface version.""" + return 1 + +@xmlrpc_method(False) +def project_list(search_str="", max_count=0): + """Get a list of projects matching the given filters.""" + try: + if len(search_str) > 0: + projects = Project.objects.filter(linkname__icontains = search_str) + else: + projects = Project.objects.all() + + if max_count > 0: + return map(project_to_dict, projects)[:max_count] + else: + return map(project_to_dict, projects) + except: + return [] + +@xmlrpc_method(False) +def project_get(project_id): + """Return structure for the given project ID.""" + try: + project = Project.objects.filter(id = project_id)[0] + return project_to_dict(project) + except: + return {} + +@xmlrpc_method(False) +def person_list(search_str="", max_count=0): + """Get a list of Person objects matching the given filters.""" + try: + if len(search_str) > 0: + people = (Person.objects.filter(name__icontains = search_str) | + Person.objects.filter(email__icontains = search_str)) + else: + people = Person.objects.all() + + if max_count > 0: + return map(person_to_dict, people)[:max_count] + else: + return map(person_to_dict, people) + + except: + return [] + +@xmlrpc_method(False) +def person_get(person_id): + """Return structure for the given person ID.""" + try: + person = Person.objects.filter(id = person_id)[0] + return person_to_dict(person) + except: + return {} + +@xmlrpc_method(False) +def patch_list(filter={}): + """Get a list of patches matching the given filters.""" + try: + # We allow access to many of the fields. But, some fields are + # filtered by raw object so we must lookup by ID instead over + # XML-RPC. + ok_fields = [ + "id", + "name", + "project_id", + "submitter_id", + "delegate_id", + "archived", + "state_id", + "date", + "commit_ref", + "hash", + "msgid", + "max_count", + ] + + dfilter = {} + max_count = 0 + + for key in filter: + parts = key.split("__") + if parts[0] not in ok_fields: + # Invalid field given + return [] + if len(parts) > 1: + if LOOKUP_TYPES.count(parts[1]) == 0: + # Invalid lookup type given + return [] + + if parts[0] == 'project_id': + dfilter['project'] = Project.objects.filter(id = + filter[key])[0] + elif parts[0] == 'submitter_id': + dfilter['submitter'] = Person.objects.filter(id = + filter[key])[0] + elif parts[0] == 'delegate_id': + dfilter['delegate'] = Person.objects.filter(id = + filter[key])[0] + elif parts[0] == 'state_id': + dfilter['state'] = State.objects.filter(id = + filter[key])[0] + elif parts[0] == 'max_count': + max_count = filter[key] + else: + dfilter[key] = filter[key] + + patches = Patch.objects.filter(**dfilter) + + if max_count > 0: + return map(patch_to_dict, patches[:max_count]) + else: + return map(patch_to_dict, patches) + + except: + return [] + +@xmlrpc_method(False) +def patch_get(patch_id): + """Return structure for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_by_hash(hash): + """Return structure for the given patch hash.""" + try: + patch = Patch.objects.filter(hash = hash)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_by_project_hash(project, hash): + """Return structure for the given patch hash.""" + try: + patch = Patch.objects.filter(project__linkname = project, + hash = hash)[0] + return patch_to_dict(patch) + except: + return {} + +@xmlrpc_method(False) +def patch_get_mbox(patch_id): + """Return mbox string for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch_to_mbox(patch).as_string(True) + except: + return "" + +@xmlrpc_method(False) +def patch_get_diff(patch_id): + """Return diff for the given patch ID.""" + try: + patch = Patch.objects.filter(id = patch_id)[0] + return patch.content + except: + return "" + +@xmlrpc_method(True) +def patch_set(user, patch_id, params): + """Update a patch with the key,value pairs in params. Only some parameters + can be set""" + try: + ok_params = ['state', 'commit_ref', 'archived'] + + patch = Patch.objects.get(id = patch_id) + + if not patch.is_editable(user): + raise Exception('No permissions to edit this patch') + + for (k, v) in params.iteritems(): + if k not in ok_params: + continue + + if k == 'state': + patch.state = State.objects.get(id = v) + + else: + setattr(patch, k, v) + + patch.save() + + return True + + except: + raise + +@xmlrpc_method(False) +def state_list(search_str="", max_count=0): + """Get a list of state structures matching the given search string.""" + try: + if len(search_str) > 0: + states = State.objects.filter(name__icontains = search_str) + else: + states = State.objects.all() + + if max_count > 0: + return map(state_to_dict, states)[:max_count] + else: + return map(state_to_dict, states) + except: + return [] + +@xmlrpc_method(False) +def state_get(state_id): + """Return structure for the given state ID.""" + try: + state = State.objects.filter(id = state_id)[0] + return state_to_dict(state) + except: + return {} |