from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect, Http404, HttpResponseForbidden
from django.template import RequestContext
from django.db import transaction
from django.db.models import Q
from django.contrib import messages
from django.contrib.auth.decorators import login_required
import settings
from datetime import datetime
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from mailqueue.util import send_mail
from models import CommitFest, Patch, PatchOnCommitFest, PatchHistory, Committer
from forms import PatchForm, NewPatchForm, CommentForm, CommitFestFilterForm
from ajax import doAttachThread
def home(request):
commitfests = list(CommitFest.objects.all())
opencf = next((c for c in commitfests if c.status == CommitFest.STATUS_OPEN), None)
inprogresscf = next((c for c in commitfests if c.status == CommitFest.STATUS_INPROGRESS), None)
return render_to_response('home.html', {
'commitfests': commitfests,
'opencf': opencf,
'inprogresscf': inprogresscf,
'title': 'Commitfests',
}, context_instance=RequestContext(request))
def commitfest(request, cfid):
# Find ourselves
cf = get_object_or_404(CommitFest, pk=cfid)
# Build a dynamic filter based on the filtering options entered
q = Q()
if request.GET.has_key('status') and request.GET['status'] != "-1":
q = q & Q(patchoncommitfest__status=int(request.GET['status']))
if request.GET.has_key('author') and request.GET['author'] != "-1":
if request.GET['author'] == '-2':
q = q & Q(authors=None)
elif request.GET['author'] == '-3':
# Checking for "yourself" requires the user to be logged in!
if not request.user.is_authenticated():
return HttpResponseRedirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
q = q & Q(authors=request.user)
else:
q = q & Q(authors__id=int(request.GET['author']))
if request.GET.has_key('reviewer') and request.GET['reviewer'] != "-1":
if request.GET['reviewer'] == '-2':
q = q & Q(reviewers=None)
elif request.GET['reviewer'] == '-3':
# Checking for "yourself" requires the user to be logged in!
if not request.user.is_authenticated():
return HttpResponseRedirect('%s?next=%s' % (settings.LOGIN_URL, request.path))
q = q & Q(reviewers=request.user)
else:
q = q & Q(reviewers__id=int(request.GET['reviewer']))
if request.GET.has_key('text') and request.GET['text'] != '':
q = q & Q(name__icontains=request.GET['text'])
has_filter = len(q.children) > 0
# Figure out custom ordering
ordering = ['-is_open', 'topic__topic', 'created',]
if request.GET.has_key('sortkey') and request.GET['sortkey']!='':
sortkey=int(request.GET['sortkey'])
if sortkey==1:
ordering = ['-is_open', 'modified', 'created',]
elif sortkey==2:
ordering = ['-is_open', 'lastmail', 'created',]
else:
sortkey=0
else:
sortkey = 0
if not has_filter and sortkey==0 and request.GET:
# Redirect to get rid of the ugly url
return HttpResponseRedirect('/%s/' % cf.id)
patches = cf.patch_set.filter(q).select_related().extra(select={
'status':'commitfest_patchoncommitfest.status',
'author_names':"SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_authors cpa ON cpa.user_id=auth_user.id WHERE cpa.patch_id=commitfest_patch.id",
'reviewer_names':"SELECT string_agg(first_name || ' ' || last_name || ' (' || username || ')', ', ') FROM auth_user INNER JOIN commitfest_patch_reviewers cpr ON cpr.user_id=auth_user.id WHERE cpr.patch_id=commitfest_patch.id",
'is_open':'commitfest_patchoncommitfest.status IN (%s)' % ','.join([str(x) for x in PatchOnCommitFest.OPEN_STATUSES]),
}).order_by(*ordering)
# Generates a fairly expensive query, which we shouldn't do unless
# the user is logged in. XXX: Figure out how to avoid doing that..
form = CommitFestFilterForm(cf, request.GET)
return render_to_response('commitfest.html', {
'cf': cf,
'form': form,
'patches': patches,
'has_filter': has_filter,
'title': 'Commitfest %s' % cf.name,
'grouping': sortkey==0,
'sortkey': sortkey,
}, context_instance=RequestContext(request))
def patch(request, cfid, patchid):
cf = get_object_or_404(CommitFest, pk=cfid)
patch = get_object_or_404(Patch.objects.select_related(), pk=patchid, commitfests=cf)
patch_commitfests = PatchOnCommitFest.objects.select_related('commitfest').filter(patch=patch).order_by('-commitfest__startdate')
#XXX: this creates a session, so find a smarter way. Probably handle
#it in the callback and just ask the user then?
if request.user.is_authenticated():
committer = list(Committer.objects.filter(user=request.user, active=True))
if len(committer) > 0:
is_committer= True
is_this_committer = committer[0] == patch.committer
else:
is_committer = is_this_committer = False
is_reviewer = request.user in patch.reviewers.all()
# is_reviewer = len([x for x in patch.reviewers.all() if x==request.user]) > 0
else:
is_committer = False
is_this_committer = False
is_reviewer = False
return render_to_response('patch.html', {
'cf': cf,
'patch': patch,
'patch_commitfests': patch_commitfests,
'is_committer': is_committer,
'is_this_committer': is_this_committer,
'is_reviewer': is_reviewer,
'title': 'View patch',
'breadcrumbs': [{'title': cf.name, 'href': '/%s/' % cf.pk},],
}, context_instance=RequestContext(request))
@login_required
@transaction.commit_on_success
def patchform(request, cfid, patchid):
cf = get_object_or_404(CommitFest, pk=cfid)
patch = get_object_or_404(Patch, pk=patchid, commitfests=cf)
if request.method == 'POST':
form = PatchForm(data=request.POST, instance=patch)
if form.is_valid():
# Some fields need to be set when creating a new one
r = form.save(commit=False)
# Fill out any locked fields here
form.save_m2m()
# Track all changes
for field, values in r.diff.items():
PatchHistory(patch=patch, by=request.user, what='Changed %s to %s' % (field, values[1])).save()
r.set_modified()
r.save()
return HttpResponseRedirect('../../%s/' % r.pk)
# Else fall through and render the page again
else:
form = PatchForm(instance=patch)
return render_to_response('base_form.html', {
'cf': cf,
'form': form,
'patch': patch,
'title': 'Edit patch',
'breadcrumbs': [{'title': cf.name, 'href': '/%s/' % cf.pk},
{'title': 'View patch', 'href': '/%s/%s/' % (cf.pk, patch.pk)}],
}, context_instance=RequestContext(request))
@login_required
@transaction.commit_on_success
def newpatch(request, cfid):
cf = get_object_or_404(CommitFest, pk=cfid)
if not cf.status == CommitFest.STATUS_OPEN and not request.user.is_staff:
raise Http404("This commitfest is not open!")
if request.method == 'POST':
form = NewPatchForm(data=request.POST)
if form.is_valid():
patch = Patch(name=form.cleaned_data['name'],
topic=form.cleaned_data['topic'])
patch.set_modified()
patch.save()
poc = PatchOnCommitFest(patch=patch, commitfest=cf, enterdate=datetime.now())
poc.save()
PatchHistory(patch=patch, by=request.user, what='Created patch record').save()
# Now add the thread
try:
doAttachThread(cf, patch, form.cleaned_data['threadmsgid'], request.user)
return HttpResponseRedirect("/%s/%s/edit/" % (cf.id, patch.id))
except Http404:
# Thread not found!
# This is a horrible breakage of API layers
form._errors['threadmsgid'] = form.error_class(('Selected thread did not exist in the archives',))
except Exception:
form._errors['threadmsgid'] = form.error_class(('An error occurred looking up the thread in the archives.',))
else:
form = NewPatchForm()
return render_to_response('base_form.html', {
'form': form,
'title': 'New patch',
'breadcrumbs': [{'title': cf.name, 'href': '/%s/' % cf.pk},],
'savebutton': 'Create patch',
'threadbrowse': True,
}, context_instance=RequestContext(request))
def _review_status_string(reviewstatus):
if '0' in reviewstatus:
if '1' in reviewstatus:
return "tested, passed"
else:
return "tested, failed"
else:
return "not tested"
@login_required
@transaction.commit_on_success
def comment(request, cfid, patchid, what):
cf = get_object_or_404(CommitFest, pk=cfid)
patch = get_object_or_404(Patch, pk=patchid)
poc = get_object_or_404(PatchOnCommitFest, patch=patch, commitfest=cf)
is_review = (what=='review')
if poc.is_closed:
messages.add_message(request, messages.INFO, "The status of this patch cannot be changed in this commitfest. You must modify it in the one where it's open!")
return HttpResponseRedirect('..')
if request.method == 'POST':
try:
form = CommentForm(patch, poc, is_review, data=request.POST)
except Exception, e:
messages.add_message(request, messages.ERROR, "Failed to build list of response options from the archives: %s" % e)
return HttpResponseRedirect('/%s/%s/' % (cf.id, patch.id))
if form.is_valid():
if is_review:
txt = "The following review has been posted through the commitfest application:\n%s\n\n%s" % (
"\n".join(["%-25s %s" % (f.label + ':', _review_status_string(form.cleaned_data[fn])) for (fn, f) in form.fields.items() if fn.startswith('review_')]),
form.cleaned_data['message']
)
else:
txt = form.cleaned_data['message']
if int(form.cleaned_data['newstatus']) != poc.status:
poc.status = int(form.cleaned_data['newstatus'])
poc.save()
PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save()
txt += "\n\nThe new status of this patch is: %s\n" % poc.statusstring
msg = MIMEText(txt, _charset='utf-8')
if form.thread.subject.startswith('Re:'):
msg['Subject'] = form.thread.subject
else:
msg['Subject'] = 'Re: %s' % form.thread.subject
msg['To'] = settings.HACKERS_EMAIL
msg['From'] = "%s %s <%s>" % (request.user.first_name, request.user.last_name, request.user.email)
msg['Date'] = formatdate(localtime=True)
msg['User-Agent'] = 'pgcommitfest'
msg['In-Reply-To'] = '<%s>' % form.respid
# We just add the "top" messageid and the one we're responding to.
# This along with in-reply-to should indicate clearly enough where
# in the thread the message belongs.
msg['References'] = '<%s> <%s>' % (form.thread.messageid, form.respid)
msg['Message-ID'] = make_msgid('pgcf')
send_mail(request.user.email, settings.HACKERS_EMAIL, msg)
PatchHistory(patch=patch, by=request.user, what='Posted %s with messageid %s' % (what, msg['Message-ID'])).save()
messages.add_message(request, messages.INFO, "Your email has been queued for pgsql-hackers, and will be sent within a few minutes.")
return HttpResponseRedirect('/%s/%s/' % (cf.id, patch.id))
else:
try:
form = CommentForm(patch, poc, is_review)
except Exception, e:
messages.add_message(request, messages.ERROR, "Failed to build list of response options from the archives: %s" % e)
return HttpResponseRedirect('/%s/%s/' % (cf.id, patch.id))
return render_to_response('base_form.html', {
'cf': cf,
'form': form,
'patch': patch,
'extraformclass': 'patchcommentform',
'breadcrumbs': [{'title': cf.name, 'href': '/%s/' % cf.pk},
{'title': 'View patch', 'href': '/%s/%s/' % (cf.pk, patch.pk)}],
'title': "Add %s" % what,
'note': 'Note! This form will generate an email to the public mailinglist pgsql-hackers, with sender set to %s!' % (request.user.email),
'savebutton': 'Send %s' % what,
}, context_instance=RequestContext(request))
@login_required
@transaction.commit_on_success
def status(request, cfid, patchid, status):
poc = get_object_or_404(PatchOnCommitFest.objects.select_related(), commitfest__id=cfid, patch__id=patchid)
if poc.is_closed:
messages.add_message(request, messages.INFO, "The status of this patch cannot be changed in this commitfest. You must modify it in the one where it's open!")
else:
if status == 'review':
newstatus = PatchOnCommitFest.STATUS_REVIEW
elif status == 'author':
newstatus = PatchOnCommitFest.STATUS_AUTHOR
elif status == 'committer':
newstatus = PatchOnCommitFest.STATUS_COMMITTER
else:
raise Exception("Can't happen")
if newstatus != poc.status:
# Only save it if something actually changed
poc.status = newstatus
poc.patch.set_modified()
poc.patch.save()
poc.save()
PatchHistory(patch=poc.patch, by=request.user, what='New status: %s' % poc.statusstring).save()
return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id))
@login_required
@transaction.commit_on_success
def close(request, cfid, patchid, status):
poc = get_object_or_404(PatchOnCommitFest.objects.select_related(), commitfest__id=cfid, patch__id=patchid)
if poc.is_closed:
messages.add_message(request, messages.INFO, "The status of this patch cannot be changed in this commitfest. You must modify it in the one where it's open!")
else:
poc.leavedate = datetime.now()
# We know the status can't be one of the ones below, since we
# have checked that we're not closed yet. Therefor, we don't
# need to check if the individual status has changed.
if status == 'reject':
poc.status = PatchOnCommitFest.STATUS_REJECTED
elif status == 'feedback':
poc.status = PatchOnCommitFest.STATUS_RETURNED
# Figure out the commitfest to actually put it on
newcf = CommitFest.objects.filter(status=CommitFest.STATUS_OPEN)
if len(newcf) == 0:
# Ok, there is no open CF at all. Let's see if there is a
# future one.
newcf = CommitFest.objects.filter(status=CommitFest.STATUS_FUTURE)
if len(newcf) == 0:
raise Exception("No open and no future commitfest exists!")
elif len(newcf) != 1:
raise Exception("No open and multiple future commitfests exists!")
elif len(newcf) != 1:
raise Exception("Multiple open commitfests exists!")
# Create a mapping to the new commitfest that we are bouncing
# this patch to.
newpoc = PatchOnCommitFest(patch=poc.patch, commitfest=newcf[0], enterdate=datetime.now())
newpoc.save()
elif status == 'committed':
poc.status = PatchOnCommitFest.STATUS_COMMITTED
#XXX: need to prompt for a committer here!
raise Exception("Need to prompt for committed if the user who just committed isn't one!")
poc.patch.committer = Committer.objects.get(user=request.user)
else:
raise Exception("Can't happen")
poc.patch.set_modified()
poc.patch.save()
poc.save()
PatchHistory(patch=poc.patch, by=request.user, what='Closed in commitfest %s with status: %s' % (poc.commitfest, poc.statusstring)).save()
return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id))
@login_required
@transaction.commit_on_success
def reviewer(request, cfid, patchid, status):
get_object_or_404(CommitFest, pk=cfid)
patch = get_object_or_404(Patch, pk=patchid)
is_reviewer = request.user in patch.reviewers.all()
if status=='become' and not is_reviewer:
patch.reviewers.add(request.user)
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Added self as reviewer').save()
elif status=='remove' and is_reviewer:
patch.reviewers.remove(request.user)
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Removed self from reviewers').save()
return HttpResponseRedirect('../../')
@login_required
@transaction.commit_on_success
def committer(request, cfid, patchid, status):
get_object_or_404(CommitFest, pk=cfid)
patch = get_object_or_404(Patch, pk=patchid)
committer = list(Committer.objects.filter(user=request.user, active=True))
if len(committer) == 0:
return HttpResponseForbidden('Only committers can do that!')
committer = committer[0]
is_committer = committer == patch.committer
if status=='become' and not is_committer:
patch.committer = committer
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Added self as committer').save()
elif status=='remove' and is_committer:
patch.committer = None
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Removed self from committers').save()
patch.save()
return HttpResponseRedirect('../../')