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, connection
from django.db.models import Q
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
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, send_simple_mail
from userprofile.util import UserWrapper
from models import CommitFest, Patch, PatchOnCommitFest, PatchHistory, Committer
from forms import PatchForm, NewPatchForm, CommentForm, CommitFestFilterForm
from forms import BulkEmailForm
from ajax import doAttachThread
from feeds import ActivityFeed
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',
'header_activity': 'Activity log',
'header_activity_link': '/activity/',
}, context_instance=RequestContext(request))
def activity(request, cfid=None, rss=None):
# Number of notes to fetch
if rss:
num = 50
else:
num = 100
if cfid:
cf = get_object_or_404(CommitFest, pk=cfid)
# Yes, we do string concatenation of the were clause. Because
# we're evil. And also because the number has been verified
# when looking up the cf itself, so nothing can be injected
# there.
extrafields = ''
where = 'WHERE poc.commitfest_id={0}'.format(cf.id)
else:
cf = None
extrafields = ',poc.commitfest_id AS cfid,cf.name AS cfname'
where = ' INNER JOIN commitfest_commitfest cf ON cf.id=poc.commitfest_id'
sql = "SELECT ph.date, auth_user.username AS by, ph.what, p.id AS patchid, p.name{0} FROM commitfest_patchhistory ph INNER JOIN commitfest_patch p ON ph.patch_id=p.id INNER JOIN auth_user on auth_user.id=ph.by_id INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id {1} ORDER BY ph.date DESC LIMIT {2}".format(extrafields,where, num)
curs = connection.cursor()
curs.execute(sql)
activity = [dict(zip([c[0] for c in curs.description],r)) for r in curs.fetchall()]
if rss:
# Return RSS feed with these objects
return ActivityFeed(activity, cf)(request)
else:
# Return regular webpage
return render_to_response('activity.html', {
'commitfest': cf,
'activity': activity,
'title': cf and 'Commitfest activity' or 'Global Commitfest activity',
'rss_alternate': cf and '/{0}/activity.rss/'.format(cf.id) or '/activity.rss/',
'rss_alternate_title': 'PostgreSQL Commitfest Activity Log',
'breadcrumbs': cf and [{'title': cf.title, 'href': '/%s/' % cf.pk},] or None,
}, context_instance=RequestContext(request))
def redir(request, what):
if what == 'open':
cf = get_object_or_404(CommitFest, status=CommitFest.STATUS_OPEN)
elif what == 'inprogress':
cf = get_object_or_404(CommitFest, status=CommitFest.STATUS_INPROGRESS)
else:
raise Http404()
return HttpResponseRedirect("/%s/" % cf.id)
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 = list(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))
# Generate patch status summary.
curs = connection.cursor()
curs.execute("SELECT ps.status, ps.statusstring, count(*) FROM commitfest_patchoncommitfest poc INNER JOIN commitfest_patchstatus ps ON ps.status=poc.status WHERE commitfest_id=%(id)s GROUP BY ps.status ORDER BY ps.sortkey", {
'id': cf.id,
})
statussummary = curs.fetchall()
statussummary.append([-1, 'Total', sum((r[2] for r in statussummary))])
# 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,
'statussummary': statussummary,
'has_filter': has_filter,
'title': cf.title,
'grouping': sortkey==0,
'sortkey': sortkey,
'openpatchids': [p.id for p in patches if p.is_open],
'header_activity': 'Activity log',
'header_activity_link': 'activity/',
}, context_instance=RequestContext(request))
def global_search(request):
if not request.GET.has_key('searchterm'):
print request.GET.keys()
return HttpResponseRedirect('/')
searchterm = request.GET['searchterm']
patches = Patch.objects.select_related().filter(name__icontains=searchterm).order_by('created',)
return render_to_response('patchsearch.html', {
'patches': patches,
'title': 'Patch search results',
}, 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')
committers = Committer.objects.filter(active=True).order_by('user__last_name', 'user__first_name')
#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 = [c for c in committers if c.user==request.user]
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()
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,
'committers': committers,
'attachnow': request.GET.has_key('attachthreadnow'),
'title': patch.name,
'breadcrumbs': [{'title': cf.title, '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.title, '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.title, '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, UserWrapper(request.user).email)
msg['Date'] = formatdate(localtime=True)
msg['User-Agent'] = 'pgcommitfest'
msg['X-cfsender'] = request.user.username
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(UserWrapper(request.user).email, settings.HACKERS_EMAIL, msg.as_string())
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.title, '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 %s, with sender set to %s!' % (settings.HACKERS_EMAIL, UserWrapper(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 exist!")
elif len(newcf) != 1:
raise Exception("Multiple open commitfests exists!")
elif newcf[0] == poc.commitfest:
# The current open CF is the same one that we are already on.
# In this case, try to see if there is a future CF we can
# move it to.
newcf = CommitFest.objects.filter(status=CommitFest.STATUS_FUTURE)
if len(newcf) == 0:
raise Exception("Cannot move patch to the same commitfest, and no future commitfests exist!")
elif len(newcf) != 1:
raise Exception("Cannot move patch to the same commitfest, and multiple future commitfests exist!")
# 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':
committer = get_object_or_404(Committer, user__username=request.GET['c'])
if committer != poc.patch.committer:
# Committer changed!
poc.patch.committer = committer
PatchHistory(patch=poc.patch, by=request.user, what='Changed committer to %s' % committer).save()
poc.status = PatchOnCommitFest.STATUS_COMMITTED
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 %s as reviewer' % request.user.username).save()
elif status=='remove' and is_reviewer:
patch.reviewers.remove(request.user)
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Removed %s from reviewers' % request.user.username).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 %s as committer' % request.user.username).save()
elif status=='remove' and is_committer:
patch.committer = None
patch.set_modified()
PatchHistory(patch=patch, by=request.user, what='Removed %s from committers' % request.user.username).save()
patch.save()
return HttpResponseRedirect('../../')
@login_required
@transaction.commit_on_success
def send_email(request, cfid):
cf = get_object_or_404(CommitFest, pk=cfid)
if not request.user.is_staff:
raise Http404("Only CF managers can do that.")
if request.method == 'POST':
authoridstring = request.POST['authors']
revieweridstring = request.POST['reviewers']
form = BulkEmailForm(data=request.POST)
if form.is_valid():
q = Q()
if authoridstring:
q = q | Q(patch_author__in=[int(x) for x in authoridstring.split(',')])
if revieweridstring:
q = q | Q(patch_reviewer__in=[int(x) for x in revieweridstring.split(',')])
recipients = User.objects.filter(q).distinct()
for r in recipients:
send_simple_mail(UserWrapper(request.user).email, r.email, form.cleaned_data['subject'], form.cleaned_data['body'], request.user.username)
messages.add_message(request, messages.INFO, "Sent email to %s" % r.email)
return HttpResponseRedirect('..')
else:
authoridstring = request.GET.get('authors', None)
revieweridstring = request.GET.get('reviewers', None)
form = BulkEmailForm(initial={'authors': authoridstring, 'reviewers': revieweridstring})
if authoridstring:
authors = list(User.objects.filter(patch_author__in=[int(x) for x in authoridstring.split(',')]).distinct())
else:
authors = []
if revieweridstring:
reviewers = list(User.objects.filter(patch_reviewer__in=[int(x) for x in revieweridstring.split(',')]).distinct())
else:
reviewers = []
if len(authors)==0 and len(reviewers)==0:
messages.add_message(request, messages.WARNING, "No recipients specified, cannot send email")
return HttpResponseRedirect('..')
messages.add_message(request, messages.INFO, "Email will be sent from: %s" % UserWrapper(request.user).email)
def _user_and_mail(u):
return "%s %s (%s)" % (u.first_name, u.last_name, u.email)
if len(authors):
messages.add_message(request, messages.INFO, "The email will be sent to the following authors: %s" % ", ".join([_user_and_mail(u) for u in authors]))
if len(reviewers):
messages.add_message(request, messages.INFO, "The email will be sent to the following reviewers: %s" % ", ".join([_user_and_mail(u) for u in reviewers]))
return render_to_response('base_form.html', {
'cf': cf,
'form': form,
'title': 'Send email',
'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},],
'savebutton': 'Send email',
}, context_instance=RequestContext(request))