From a32f4007670330d05a834db1b67687ab8b8c3b34 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Tue, 5 Feb 2019 23:01:05 +0100 Subject: [PATCH] Convert all spaces to tabs --- pgcommitfest/auth.py | 262 ++-- pgcommitfest/commitfest/admin.py | 14 +- pgcommitfest/commitfest/ajax.py | 390 ++--- pgcommitfest/commitfest/feeds.py | 58 +- pgcommitfest/commitfest/forms.py | 198 +-- pgcommitfest/commitfest/lookups.py | 26 +- .../management/commands/send_notifications.py | 64 +- .../migrations/0003_withdrawn_status.py | 4 +- pgcommitfest/commitfest/models.py | 562 ++++---- pgcommitfest/commitfest/reports.py | 24 +- .../commitfest/templatetags/commitfest.py | 30 +- pgcommitfest/commitfest/util.py | 66 +- pgcommitfest/commitfest/views.py | 1270 ++++++++--------- pgcommitfest/commitfest/widgets.py | 10 +- pgcommitfest/mailqueue/models.py | 14 +- pgcommitfest/mailqueue/util.py | 60 +- pgcommitfest/settings.py | 36 +- pgcommitfest/userprofile/admin.py | 2 +- pgcommitfest/userprofile/forms.py | 54 +- pgcommitfest/userprofile/models.py | 42 +- pgcommitfest/userprofile/util.py | 52 +- pgcommitfest/userprofile/views.py | 130 +- 22 files changed, 1684 insertions(+), 1684 deletions(-) diff --git a/pgcommitfest/auth.py b/pgcommitfest/auth.py index 13ada6a..1073b5e 100644 --- a/pgcommitfest/auth.py +++ b/pgcommitfest/auth.py @@ -36,10 +36,10 @@ from Crypto import Random import time class AuthBackend(ModelBackend): - # We declare a fake backend that always fails direct authentication - - # since we should never be using direct authentication in the first place! - def authenticate(self, username=None, password=None): - raise Exception("Direct authentication not supported") + # We declare a fake backend that always fails direct authentication - + # since we should never be using direct authentication in the first place! + def authenticate(self, username=None, password=None): + raise Exception("Direct authentication not supported") #### @@ -48,85 +48,85 @@ class AuthBackend(ModelBackend): # Handle login requests by sending them off to the main site def login(request): - if request.GET.has_key('next'): - # Put together an url-encoded dict of parameters we're getting back, - # including a small nonce at the beginning to make sure it doesn't - # encrypt the same way every time. - s = "t=%s&%s" % (int(time.time()), urllib.urlencode({'r': request.GET['next']})) - # Now encrypt it - r = Random.new() - iv = r.read(16) - encryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], AES.MODE_CBC, iv) - cipher = encryptor.encrypt(s + ' ' * (16-(len(s) % 16))) # pad to 16 bytes - - return HttpResponseRedirect("%s?d=%s$%s" % ( - settings.PGAUTH_REDIRECT, - base64.b64encode(iv, "-_"), - base64.b64encode(cipher, "-_"), - )) - else: - return HttpResponseRedirect(settings.PGAUTH_REDIRECT) + if request.GET.has_key('next'): + # Put together an url-encoded dict of parameters we're getting back, + # including a small nonce at the beginning to make sure it doesn't + # encrypt the same way every time. + s = "t=%s&%s" % (int(time.time()), urllib.urlencode({'r': request.GET['next']})) + # Now encrypt it + r = Random.new() + iv = r.read(16) + encryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], AES.MODE_CBC, iv) + cipher = encryptor.encrypt(s + ' ' * (16-(len(s) % 16))) # pad to 16 bytes + + return HttpResponseRedirect("%s?d=%s$%s" % ( + settings.PGAUTH_REDIRECT, + base64.b64encode(iv, "-_"), + base64.b64encode(cipher, "-_"), + )) + else: + return HttpResponseRedirect(settings.PGAUTH_REDIRECT) # Handle logout requests by logging out of this site and then # redirecting to log out from the main site as well. def logout(request): - if request.user.is_authenticated(): - django_logout(request) - return HttpResponseRedirect("%slogout/" % settings.PGAUTH_REDIRECT) + if request.user.is_authenticated(): + django_logout(request) + return HttpResponseRedirect("%slogout/" % settings.PGAUTH_REDIRECT) # Receive an authentication response from the main website and try # to log the user in. def auth_receive(request): - if request.GET.has_key('s') and request.GET['s'] == "logout": - # This was a logout request - return HttpResponseRedirect('/') - - if not request.GET.has_key('i'): - return HttpResponse("Missing IV in url!", status=400) - if not request.GET.has_key('d'): - return HttpResponse("Missing data in url!", status=400) - - # Set up an AES object and decrypt the data we received - decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), - AES.MODE_CBC, - base64.b64decode(str(request.GET['i']), "-_")) - s = decryptor.decrypt(base64.b64decode(str(request.GET['d']), "-_")).rstrip(' ') - - # Now un-urlencode it - try: - data = urlparse.parse_qs(s, strict_parsing=True) - except ValueError: - return HttpResponse("Invalid encrypted data received.", status=400) - - # Check the timestamp in the authentication - if (int(data['t'][0]) < time.time() - 10): - return HttpResponse("Authentication token too old.", status=400) - - # Update the user record (if any) - try: - user = User.objects.get(username=data['u'][0]) - # User found, let's see if any important fields have changed - changed = False - if user.first_name != data['f'][0]: - user.first_name = data['f'][0] - changed = True - if user.last_name != data['l'][0]: - user.last_name = data['l'][0] - changed = True - if user.email != data['e'][0]: - user.email = data['e'][0] - changed= True - if changed: - user.save() - except User.DoesNotExist: - # User not found, create it! - - # NOTE! We have some legacy users where there is a user in - # the database with a different userid. Instead of trying to - # somehow fix that live, give a proper error message and - # have somebody look at it manually. - if User.objects.filter(email=data['e'][0]).exists(): - return HttpResponse("""A user with email %s already exists, but with + if request.GET.has_key('s') and request.GET['s'] == "logout": + # This was a logout request + return HttpResponseRedirect('/') + + if not request.GET.has_key('i'): + return HttpResponse("Missing IV in url!", status=400) + if not request.GET.has_key('d'): + return HttpResponse("Missing data in url!", status=400) + + # Set up an AES object and decrypt the data we received + decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), + AES.MODE_CBC, + base64.b64decode(str(request.GET['i']), "-_")) + s = decryptor.decrypt(base64.b64decode(str(request.GET['d']), "-_")).rstrip(' ') + + # Now un-urlencode it + try: + data = urlparse.parse_qs(s, strict_parsing=True) + except ValueError: + return HttpResponse("Invalid encrypted data received.", status=400) + + # Check the timestamp in the authentication + if (int(data['t'][0]) < time.time() - 10): + return HttpResponse("Authentication token too old.", status=400) + + # Update the user record (if any) + try: + user = User.objects.get(username=data['u'][0]) + # User found, let's see if any important fields have changed + changed = False + if user.first_name != data['f'][0]: + user.first_name = data['f'][0] + changed = True + if user.last_name != data['l'][0]: + user.last_name = data['l'][0] + changed = True + if user.email != data['e'][0]: + user.email = data['e'][0] + changed= True + if changed: + user.save() + except User.DoesNotExist: + # User not found, create it! + + # NOTE! We have some legacy users where there is a user in + # the database with a different userid. Instead of trying to + # somehow fix that live, give a proper error message and + # have somebody look at it manually. + if User.objects.filter(email=data['e'][0]).exists(): + return HttpResponse("""A user with email %s already exists, but with a different username than %s. This is almost certainly caused by some legacy data in our database. @@ -137,39 +137,39 @@ for you. We apologize for the inconvenience. """ % (data['e'][0], data['u'][0]), content_type='text/plain') - user = User(username=data['u'][0], - first_name=data['f'][0], - last_name=data['l'][0], - email=data['e'][0], - password='setbypluginnotasha1', - ) - user.save() - - # Ok, we have a proper user record. Now tell django that - # we're authenticated so it persists it in the session. Before - # we do that, we have to annotate it with the backend information. - user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__) - django_login(request, user) - - # Finally, check of we have a data package that tells us where to - # redirect the user. - if data.has_key('d'): - (ivs, datas) = data['d'][0].split('$') - decryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], - AES.MODE_CBC, - base64.b64decode(ivs, "-_")) - s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ') - try: - rdata = urlparse.parse_qs(s, strict_parsing=True) - except ValueError: - return HttpResponse("Invalid encrypted data received.", status=400) - if rdata.has_key('r'): - # Redirect address - return HttpResponseRedirect(rdata['r'][0]) - # No redirect specified, see if we have it in our settings - if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'): - return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS) - return HttpResponse("Authentication successful, but don't know where to redirect!", status=500) + user = User(username=data['u'][0], + first_name=data['f'][0], + last_name=data['l'][0], + email=data['e'][0], + password='setbypluginnotasha1', + ) + user.save() + + # Ok, we have a proper user record. Now tell django that + # we're authenticated so it persists it in the session. Before + # we do that, we have to annotate it with the backend information. + user.backend = "%s.%s" % (AuthBackend.__module__, AuthBackend.__name__) + django_login(request, user) + + # Finally, check of we have a data package that tells us where to + # redirect the user. + if data.has_key('d'): + (ivs, datas) = data['d'][0].split('$') + decryptor = AES.new(SHA.new(settings.SECRET_KEY).digest()[:16], + AES.MODE_CBC, + base64.b64decode(ivs, "-_")) + s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ') + try: + rdata = urlparse.parse_qs(s, strict_parsing=True) + except ValueError: + return HttpResponse("Invalid encrypted data received.", status=400) + if rdata.has_key('r'): + # Redirect address + return HttpResponseRedirect(rdata['r'][0]) + # No redirect specified, see if we have it in our settings + if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'): + return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS) + return HttpResponse("Authentication successful, but don't know where to redirect!", status=500) # Perform a search in the central system. Note that the results are returned as an @@ -180,26 +180,26 @@ We apologize for the inconvenience. # Unlike the authentication, searching does not involve the browser - we just make # a direct http call. def user_search(searchterm=None, userid=None): - # If upsteam isn't responding quickly, it's not going to respond at all, and - # 10 seconds is already quite long. - socket.setdefaulttimeout(10) - if userid: - q = {'u': userid} - else: - q = {'s': searchterm} - - u = urllib.urlopen('%ssearch/?%s' % ( - settings.PGAUTH_REDIRECT, - urllib.urlencode(q), - )) - (ivs, datas) = u.read().split('&') - u.close() - - # Decryption time - decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), - AES.MODE_CBC, - base64.b64decode(ivs, "-_")) - s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ') - j = json.loads(s) - - return j + # If upsteam isn't responding quickly, it's not going to respond at all, and + # 10 seconds is already quite long. + socket.setdefaulttimeout(10) + if userid: + q = {'u': userid} + else: + q = {'s': searchterm} + + u = urllib.urlopen('%ssearch/?%s' % ( + settings.PGAUTH_REDIRECT, + urllib.urlencode(q), + )) + (ivs, datas) = u.read().split('&') + u.close() + + # Decryption time + decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), + AES.MODE_CBC, + base64.b64decode(ivs, "-_")) + s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ') + j = json.loads(s) + + return j diff --git a/pgcommitfest/commitfest/admin.py b/pgcommitfest/commitfest/admin.py index 05fe3ed..232ba30 100644 --- a/pgcommitfest/commitfest/admin.py +++ b/pgcommitfest/commitfest/admin.py @@ -3,19 +3,19 @@ from django.contrib import admin from models import * class CommitterAdmin(admin.ModelAdmin): - list_display = ('user', 'active') + list_display = ('user', 'active') class PatchOnCommitFestInline(admin.TabularInline): - model = PatchOnCommitFest - extra=1 + model = PatchOnCommitFest + extra=1 class PatchAdmin(admin.ModelAdmin): - inlines = (PatchOnCommitFestInline,) - list_display = ('name', ) -# list_filter = ('commitfests_set__commitfest__name',) + inlines = (PatchOnCommitFestInline,) + list_display = ('name', ) +# list_filter = ('commitfests_set__commitfest__name',) class MailThreadAttachmentAdmin(admin.ModelAdmin): - list_display = ('date', 'author', 'messageid', 'mailthread',) + list_display = ('date', 'author', 'messageid', 'mailthread',) admin.site.register(Committer, CommitterAdmin) admin.site.register(CommitFest) diff --git a/pgcommitfest/commitfest/ajax.py b/pgcommitfest/commitfest/ajax.py index 3ec76ea..b8317f5 100644 --- a/pgcommitfest/commitfest/ajax.py +++ b/pgcommitfest/commitfest/ajax.py @@ -12,249 +12,249 @@ import json from pgcommitfest.auth import user_search class HttpResponseServiceUnavailable(HttpResponse): - status_code = 503 + status_code = 503 class Http503(Exception): - pass + pass from models import CommitFest, Patch, MailThread, MailThreadAttachment from models import MailThreadAnnotation, PatchHistory def _archivesAPI(suburl, params=None): - try: - resp = requests.get("http{0}://{1}:{2}{3}".format(settings.ARCHIVES_PORT == 443 and 's' or '', - settings.ARCHIVES_SERVER, - settings.ARCHIVES_PORT, - suburl), - params=params, - headers={ - 'Host': settings.ARCHIVES_HOST, - }, - timeout=settings.ARCHIVES_TIMEOUT, - ) - if resp.status_code != 200: - if resp.status_code == 404: - raise Http404() - raise Exception("JSON call failed: %s" % resp.status_code) - - return resp.json() - except Http404: - raise - except Exception as e: - raise Http503("Failed to communicate with archives backend: %s" % e) + try: + resp = requests.get("http{0}://{1}:{2}{3}".format(settings.ARCHIVES_PORT == 443 and 's' or '', + settings.ARCHIVES_SERVER, + settings.ARCHIVES_PORT, + suburl), + params=params, + headers={ + 'Host': settings.ARCHIVES_HOST, + }, + timeout=settings.ARCHIVES_TIMEOUT, + ) + if resp.status_code != 200: + if resp.status_code == 404: + raise Http404() + raise Exception("JSON call failed: %s" % resp.status_code) + + return resp.json() + except Http404: + raise + except Exception as e: + raise Http503("Failed to communicate with archives backend: %s" % e) def getThreads(request): - search = request.GET.has_key('s') and request.GET['s'] or None - if request.GET.has_key('a') and request.GET['a'] == "1": - attachonly = 1 - else: - attachonly = 0 + search = request.GET.has_key('s') and request.GET['s'] or None + if request.GET.has_key('a') and request.GET['a'] == "1": + attachonly = 1 + else: + attachonly = 0 - # Make a JSON api call to the archives server - params = {'n': 100, 'a': attachonly} - if search: - params['s'] = search + # Make a JSON api call to the archives server + params = {'n': 100, 'a': attachonly} + if search: + params['s'] = search - r = _archivesAPI('/list/pgsql-hackers/latest.json', params) - return sorted(r, key=lambda x: x['date'], reverse=True) + r = _archivesAPI('/list/pgsql-hackers/latest.json', params) + return sorted(r, key=lambda x: x['date'], reverse=True) def getMessages(request): - threadid = request.GET['t'] + threadid = request.GET['t'] - thread = MailThread.objects.get(pk=threadid) + thread = MailThread.objects.get(pk=threadid) - # Always make a call over to the archives api - r = _archivesAPI('/message-id.json/%s' % thread.messageid) - return sorted(r, key=lambda x: x['date'], reverse=True) + # Always make a call over to the archives api + r = _archivesAPI('/message-id.json/%s' % thread.messageid) + return sorted(r, key=lambda x: x['date'], reverse=True) def refresh_single_thread(thread): - r = sorted(_archivesAPI('/message-id.json/%s' % thread.messageid), key=lambda x: x['date']) - if thread.latestmsgid != r[-1]['msgid']: - # There is now a newer mail in the thread! - thread.latestmsgid = r[-1]['msgid'] - thread.latestmessage = r[-1]['date'] - thread.latestauthor = r[-1]['from'] - thread.latestsubject = r[-1]['subj'] - thread.save() - parse_and_add_attachments(r, thread) - # Potentially update the last mail date - if there wasn't already a mail on each patch - # from a *different* thread that had an earlier date. - for p in thread.patches.filter(lastmail__lt=thread.latestmessage): - p.lastmail = thread.latestmessage - p.save() + r = sorted(_archivesAPI('/message-id.json/%s' % thread.messageid), key=lambda x: x['date']) + if thread.latestmsgid != r[-1]['msgid']: + # There is now a newer mail in the thread! + thread.latestmsgid = r[-1]['msgid'] + thread.latestmessage = r[-1]['date'] + thread.latestauthor = r[-1]['from'] + thread.latestsubject = r[-1]['subj'] + thread.save() + parse_and_add_attachments(r, thread) + # Potentially update the last mail date - if there wasn't already a mail on each patch + # from a *different* thread that had an earlier date. + for p in thread.patches.filter(lastmail__lt=thread.latestmessage): + p.lastmail = thread.latestmessage + p.save() @transaction.atomic def annotateMessage(request): - thread = get_object_or_404(MailThread, pk=int(request.POST['t'])) - msgid = request.POST['msgid'] - msg = request.POST['msg'] - - # Get the subject, author and date from the archives - # We only have an API call to get the whole thread right now, so - # do that, and then find our entry in it. - r = _archivesAPI('/message-id.json/%s' % thread.messageid) - for m in r: - if m['msgid'] == msgid: - annotation = MailThreadAnnotation(mailthread=thread, - user=request.user, - msgid=msgid, - annotationtext=msg, - mailsubject=m['subj'], - maildate=m['date'], - mailauthor=m['from']) - annotation.save() - - for p in thread.patches.all(): - PatchHistory(patch=p, by=request.user, what='Added annotation "%s" to %s' % (msg, msgid)).save_and_notify() - p.set_modified() - p.save() - - return 'OK' - return 'Message not found in thread!' + thread = get_object_or_404(MailThread, pk=int(request.POST['t'])) + msgid = request.POST['msgid'] + msg = request.POST['msg'] + + # Get the subject, author and date from the archives + # We only have an API call to get the whole thread right now, so + # do that, and then find our entry in it. + r = _archivesAPI('/message-id.json/%s' % thread.messageid) + for m in r: + if m['msgid'] == msgid: + annotation = MailThreadAnnotation(mailthread=thread, + user=request.user, + msgid=msgid, + annotationtext=msg, + mailsubject=m['subj'], + maildate=m['date'], + mailauthor=m['from']) + annotation.save() + + for p in thread.patches.all(): + PatchHistory(patch=p, by=request.user, what='Added annotation "%s" to %s' % (msg, msgid)).save_and_notify() + p.set_modified() + p.save() + + return 'OK' + return 'Message not found in thread!' @transaction.atomic def deleteAnnotation(request): - annotation = get_object_or_404(MailThreadAnnotation, pk=request.POST['id']) + annotation = get_object_or_404(MailThreadAnnotation, pk=request.POST['id']) - for p in annotation.mailthread.patches.all(): - PatchHistory(patch=p, by=request.user, what='Deleted annotation "%s" from %s' % (annotation.annotationtext, annotation.msgid)).save_and_notify() - p.set_modified() - p.save() + for p in annotation.mailthread.patches.all(): + PatchHistory(patch=p, by=request.user, what='Deleted annotation "%s" from %s' % (annotation.annotationtext, annotation.msgid)).save_and_notify() + p.set_modified() + p.save() - annotation.delete() + annotation.delete() - return 'OK' + return 'OK' def parse_and_add_attachments(threadinfo, mailthread): - for t in threadinfo: - if len(t['atts']): - # One or more attachments. For now, we're only actually going - # to store and process the first one, even though the API gets - # us all of them. - MailThreadAttachment.objects.get_or_create(mailthread=mailthread, - messageid=t['msgid'], - defaults={ - 'date': t['date'], - 'author': t['from'], - 'attachmentid': t['atts'][0]['id'], - 'filename': t['atts'][0]['name'], - }) - # In theory we should remove objects if they don't have an - # attachment, but how could that ever happen? Ignore for now. + for t in threadinfo: + if len(t['atts']): + # One or more attachments. For now, we're only actually going + # to store and process the first one, even though the API gets + # us all of them. + MailThreadAttachment.objects.get_or_create(mailthread=mailthread, + messageid=t['msgid'], + defaults={ + 'date': t['date'], + 'author': t['from'], + 'attachmentid': t['atts'][0]['id'], + 'filename': t['atts'][0]['name'], + }) + # In theory we should remove objects if they don't have an + # attachment, but how could that ever happen? Ignore for now. @transaction.atomic def attachThread(request): - cf = get_object_or_404(CommitFest, pk=int(request.POST['cf'])) - patch = get_object_or_404(Patch, pk=int(request.POST['p']), commitfests=cf) - msgid = request.POST['msg'] + cf = get_object_or_404(CommitFest, pk=int(request.POST['cf'])) + patch = get_object_or_404(Patch, pk=int(request.POST['p']), commitfests=cf) + msgid = request.POST['msg'] - return doAttachThread(cf, patch, msgid, request.user) + return doAttachThread(cf, patch, msgid, request.user) def doAttachThread(cf, patch, msgid, user): - # Note! Must be called in an open transaction! - r = sorted(_archivesAPI('/message-id.json/%s' % msgid), key=lambda x: x['date']) - # We have the full thread metadata - using the first and last entry, - # construct a new mailthread in our own model. - # First, though, check if it's already there. - threads = MailThread.objects.filter(messageid=r[0]['msgid']) - if len(threads): - thread = threads[0] - if thread.patches.filter(id=patch.id).exists(): - return 'This thread is already added to this email' - - # We did not exist, so we'd better add ourselves. - # While at it, we update the thread entry with the latest data from the - # archives. - thread.patches.add(patch) - thread.latestmessage=r[-1]['date'] - thread.latestauthor=r[-1]['from'] - thread.latestsubject=r[-1]['subj'] - thread.latestmsgid=r[-1]['msgid'] - thread.save() - else: - # No existing thread existed, so create it - # Now create a new mailthread entry - m = MailThread(messageid=r[0]['msgid'], - subject=r[0]['subj'], - firstmessage=r[0]['date'], - firstauthor=r[0]['from'], - latestmessage=r[-1]['date'], - latestauthor=r[-1]['from'], - latestsubject=r[-1]['subj'], - latestmsgid=r[-1]['msgid'], - ) - m.save() - m.patches.add(patch) - m.save() - parse_and_add_attachments(r, m) - - PatchHistory(patch=patch, by=user, what='Attached mail thread %s' % r[0]['msgid']).save_and_notify() - patch.update_lastmail() - patch.set_modified() - patch.save() - - return 'OK' + # Note! Must be called in an open transaction! + r = sorted(_archivesAPI('/message-id.json/%s' % msgid), key=lambda x: x['date']) + # We have the full thread metadata - using the first and last entry, + # construct a new mailthread in our own model. + # First, though, check if it's already there. + threads = MailThread.objects.filter(messageid=r[0]['msgid']) + if len(threads): + thread = threads[0] + if thread.patches.filter(id=patch.id).exists(): + return 'This thread is already added to this email' + + # We did not exist, so we'd better add ourselves. + # While at it, we update the thread entry with the latest data from the + # archives. + thread.patches.add(patch) + thread.latestmessage=r[-1]['date'] + thread.latestauthor=r[-1]['from'] + thread.latestsubject=r[-1]['subj'] + thread.latestmsgid=r[-1]['msgid'] + thread.save() + else: + # No existing thread existed, so create it + # Now create a new mailthread entry + m = MailThread(messageid=r[0]['msgid'], + subject=r[0]['subj'], + firstmessage=r[0]['date'], + firstauthor=r[0]['from'], + latestmessage=r[-1]['date'], + latestauthor=r[-1]['from'], + latestsubject=r[-1]['subj'], + latestmsgid=r[-1]['msgid'], + ) + m.save() + m.patches.add(patch) + m.save() + parse_and_add_attachments(r, m) + + PatchHistory(patch=patch, by=user, what='Attached mail thread %s' % r[0]['msgid']).save_and_notify() + patch.update_lastmail() + patch.set_modified() + patch.save() + + return 'OK' @transaction.atomic def detachThread(request): - cf = get_object_or_404(CommitFest, pk=int(request.POST['cf'])) - patch = get_object_or_404(Patch, pk=int(request.POST['p']), commitfests=cf) - thread = get_object_or_404(MailThread, messageid=request.POST['msg']) + cf = get_object_or_404(CommitFest, pk=int(request.POST['cf'])) + patch = get_object_or_404(Patch, pk=int(request.POST['p']), commitfests=cf) + thread = get_object_or_404(MailThread, messageid=request.POST['msg']) - patch.mailthread_set.remove(thread) - PatchHistory(patch=patch, by=request.user, what='Detached mail thread %s' % request.POST['msg']).save_and_notify() - patch.update_lastmail() - patch.set_modified() - patch.save() + patch.mailthread_set.remove(thread) + PatchHistory(patch=patch, by=request.user, what='Detached mail thread %s' % request.POST['msg']).save_and_notify() + patch.update_lastmail() + patch.set_modified() + patch.save() - return 'OK' + return 'OK' def searchUsers(request): - if request.GET.has_key('s') and request.GET['s']: - return user_search(request.GET['s']) - else: - return [] + if request.GET.has_key('s') and request.GET['s']: + return user_search(request.GET['s']) + else: + return [] def importUser(request): - if request.GET.has_key('u') and request.GET['u']: - u = user_search(userid=request.GET['u']) - if len (u) != 1: - return "Internal error, duplicate user found" - - u = u[0] - - if User.objects.filter(username=u['u']).exists(): - return "User already exists" - User(username=u['u'], - first_name=u['f'], - last_name=u['l'], - email=u['e'], - password='setbypluginnotsha1', - ).save() - return 'OK' - else: - raise Http404() + if request.GET.has_key('u') and request.GET['u']: + u = user_search(userid=request.GET['u']) + if len (u) != 1: + return "Internal error, duplicate user found" + + u = u[0] + + if User.objects.filter(username=u['u']).exists(): + return "User already exists" + User(username=u['u'], + first_name=u['f'], + last_name=u['l'], + email=u['e'], + password='setbypluginnotsha1', + ).save() + return 'OK' + else: + raise Http404() _ajax_map={ - 'getThreads': getThreads, - 'getMessages': getMessages, - 'attachThread': attachThread, - 'detachThread': detachThread, - 'annotateMessage': annotateMessage, - 'deleteAnnotation': deleteAnnotation, - 'searchUsers': searchUsers, - 'importUser': importUser, + 'getThreads': getThreads, + 'getMessages': getMessages, + 'attachThread': attachThread, + 'detachThread': detachThread, + 'annotateMessage': annotateMessage, + 'deleteAnnotation': deleteAnnotation, + 'searchUsers': searchUsers, + 'importUser': importUser, } # Main entrypoint for /ajax// @csrf_exempt @login_required def main(request, command): - if not _ajax_map.has_key(command): - raise Http404 - try: - resp = HttpResponse(content_type='application/json') - json.dump(_ajax_map[command](request), resp) - return resp - except Http503, e: - return HttpResponseServiceUnavailable(e, content_type='text/plain') + if not _ajax_map.has_key(command): + raise Http404 + try: + resp = HttpResponse(content_type='application/json') + json.dump(_ajax_map[command](request), resp) + return resp + except Http503, e: + return HttpResponseServiceUnavailable(e, content_type='text/plain') diff --git a/pgcommitfest/commitfest/feeds.py b/pgcommitfest/commitfest/feeds.py index 025ddef..d858703 100644 --- a/pgcommitfest/commitfest/feeds.py +++ b/pgcommitfest/commitfest/feeds.py @@ -1,38 +1,38 @@ from django.contrib.syndication.views import Feed class ActivityFeed(Feed): - title = description = 'Commitfest Activity Log' - link = 'https://commitfest.postgresql.org/' + title = description = 'Commitfest Activity Log' + link = 'https://commitfest.postgresql.org/' - def __init__(self, activity, cf, *args, **kwargs): - super(ActivityFeed, self).__init__(*args, **kwargs) - self.activity = activity - if cf: - self.cfid = cf.id - self.title = self.description = 'PostgreSQL Commitfest {0} Activity Log'.format(cf.name) - else: - self.cfid = None + def __init__(self, activity, cf, *args, **kwargs): + super(ActivityFeed, self).__init__(*args, **kwargs) + self.activity = activity + if cf: + self.cfid = cf.id + self.title = self.description = 'PostgreSQL Commitfest {0} Activity Log'.format(cf.name) + else: + self.cfid = None - def items(self): - return self.activity + def items(self): + return self.activity - def item_title(self, item): - if self.cfid: - return item['name'] - else: - return u'{cfname}: {name}'.format(**item) + def item_title(self, item): + if self.cfid: + return item['name'] + else: + return u'{cfname}: {name}'.format(**item) - def item_description(self, item): - if self.cfid: - return u"
Patch: {name}
User: {by}
\n
{what}
".format(**item) - else: - return u"
Commitfest: {cfname}
Patch: {name}
User: {by}
{what}
".format(**item) + def item_description(self, item): + if self.cfid: + return u"
Patch: {name}
User: {by}
\n
{what}
".format(**item) + else: + return u"
Commitfest: {cfname}
Patch: {name}
User: {by}
{what}
".format(**item) - def item_link(self, item): - if self.cfid: - return 'https://commitfest.postgresql.org/{cfid}/{patchid}/'.format(cfid=self.cfid,**item) - else: - return 'https://commitfest.postgresql.org/{cfid}/{patchid}/'.format(**item) + def item_link(self, item): + if self.cfid: + return 'https://commitfest.postgresql.org/{cfid}/{patchid}/'.format(cfid=self.cfid,**item) + else: + return 'https://commitfest.postgresql.org/{cfid}/{patchid}/'.format(**item) - def item_pubdate(self, item): - return item['date'] + def item_pubdate(self, item): + return item['date'] diff --git a/pgcommitfest/commitfest/forms.py b/pgcommitfest/commitfest/forms.py index 568af64..a341f99 100644 --- a/pgcommitfest/commitfest/forms.py +++ b/pgcommitfest/commitfest/forms.py @@ -13,125 +13,125 @@ from widgets import ThreadPickWidget from ajax import _archivesAPI class CommitFestFilterForm(forms.Form): - text = forms.CharField(max_length=50, required=False) - status = forms.ChoiceField(required=False) - author = forms.ChoiceField(required=False) - reviewer = forms.ChoiceField(required=False) - sortkey = forms.IntegerField(required=False) + text = forms.CharField(max_length=50, required=False) + status = forms.ChoiceField(required=False) + author = forms.ChoiceField(required=False) + reviewer = forms.ChoiceField(required=False) + sortkey = forms.IntegerField(required=False) - def __init__(self, cf, *args, **kwargs): - super(CommitFestFilterForm, self).__init__(*args, **kwargs) + def __init__(self, cf, *args, **kwargs): + super(CommitFestFilterForm, self).__init__(*args, **kwargs) - self.fields['sortkey'].widget = forms.HiddenInput() + self.fields['sortkey'].widget = forms.HiddenInput() - c = [(-1, '* All')] + list(PatchOnCommitFest._STATUS_CHOICES) - self.fields['status'] = forms.ChoiceField(choices=c, required=False) + c = [(-1, '* All')] + list(PatchOnCommitFest._STATUS_CHOICES) + self.fields['status'] = forms.ChoiceField(choices=c, required=False) - q = Q(patch_author__commitfests=cf) | Q(patch_reviewer__commitfests=cf) - userchoices = [(-1, '* All'), (-2, '* None'), (-3, '* Yourself') ] + [(u.id, '%s %s (%s)' % (u.first_name, u.last_name, u.username)) for u in User.objects.filter(q).distinct().order_by('first_name', 'last_name')] - self.fields['author'] = forms.ChoiceField(choices=userchoices, required=False) - self.fields['reviewer'] = forms.ChoiceField(choices=userchoices, required=False) + q = Q(patch_author__commitfests=cf) | Q(patch_reviewer__commitfests=cf) + userchoices = [(-1, '* All'), (-2, '* None'), (-3, '* Yourself') ] + [(u.id, '%s %s (%s)' % (u.first_name, u.last_name, u.username)) for u in User.objects.filter(q).distinct().order_by('first_name', 'last_name')] + self.fields['author'] = forms.ChoiceField(choices=userchoices, required=False) + self.fields['reviewer'] = forms.ChoiceField(choices=userchoices, required=False) - for f in ('status', 'author', 'reviewer',): - self.fields[f].widget.attrs = {'class': 'input-medium'} + for f in ('status', 'author', 'reviewer',): + self.fields[f].widget.attrs = {'class': 'input-medium'} class PatchForm(forms.ModelForm): - class Meta: - model = Patch - exclude = ('commitfests', 'mailthreads', 'modified', 'lastmail', 'subscribers', ) - widgets = { - 'authors': AutoCompleteSelectMultipleWidget(lookup_class=UserLookup, position='top'), - 'reviewers': AutoCompleteSelectMultipleWidget(lookup_class=UserLookup, position='top'), - } + class Meta: + model = Patch + exclude = ('commitfests', 'mailthreads', 'modified', 'lastmail', 'subscribers', ) + widgets = { + 'authors': AutoCompleteSelectMultipleWidget(lookup_class=UserLookup, position='top'), + 'reviewers': AutoCompleteSelectMultipleWidget(lookup_class=UserLookup, position='top'), + } - def __init__(self, *args, **kwargs): - super(PatchForm, self).__init__(*args, **kwargs) - self.fields['authors'].help_text = 'Enter part of name to see list' - self.fields['reviewers'].help_text = 'Enter part of name to see list' - self.fields['committer'].label_from_instance = lambda x: '%s %s (%s)' % (x.user.first_name, x.user.last_name, x.user.username) + def __init__(self, *args, **kwargs): + super(PatchForm, self).__init__(*args, **kwargs) + self.fields['authors'].help_text = 'Enter part of name to see list' + self.fields['reviewers'].help_text = 'Enter part of name to see list' + self.fields['committer'].label_from_instance = lambda x: '%s %s (%s)' % (x.user.first_name, x.user.last_name, x.user.username) class NewPatchForm(forms.ModelForm): - threadmsgid = forms.CharField(max_length=200, required=True, label='Specify thread msgid', widget=ThreadPickWidget) -# patchfile = forms.FileField(allow_empty_file=False, max_length=50000, label='or upload patch file', required=False, help_text='This may be supported sometime in the future, and would then autogenerate a mail to the hackers list. At such a time, the threadmsgid would no longer be required.') - - class Meta: - model = Patch - exclude = ('commitfests', 'mailthreads', 'modified', 'authors', 'reviewers', 'committer', 'wikilink', 'gitlink', 'lastmail', 'subscribers', ) - - def clean_threadmsgid(self): - try: - _archivesAPI('/message-id.json/%s' % self.cleaned_data['threadmsgid']) - except Http404: - raise ValidationError("Message not found in archives") - except: - raise ValidationError("Error in API call to validate thread") - return self.cleaned_data['threadmsgid'] + threadmsgid = forms.CharField(max_length=200, required=True, label='Specify thread msgid', widget=ThreadPickWidget) +# patchfile = forms.FileField(allow_empty_file=False, max_length=50000, label='or upload patch file', required=False, help_text='This may be supported sometime in the future, and would then autogenerate a mail to the hackers list. At such a time, the threadmsgid would no longer be required.') + + class Meta: + model = Patch + exclude = ('commitfests', 'mailthreads', 'modified', 'authors', 'reviewers', 'committer', 'wikilink', 'gitlink', 'lastmail', 'subscribers', ) + + def clean_threadmsgid(self): + try: + _archivesAPI('/message-id.json/%s' % self.cleaned_data['threadmsgid']) + except Http404: + raise ValidationError("Message not found in archives") + except: + raise ValidationError("Error in API call to validate thread") + return self.cleaned_data['threadmsgid'] def _fetch_thread_choices(patch): - for mt in patch.mailthread_set.order_by('-latestmessage'): - ti = sorted(_archivesAPI('/message-id.json/%s' % mt.messageid), key=lambda x: x['date'], reverse=True) - yield [mt.subject, - [('%s,%s' % (mt.messageid, t['msgid']),'From %s at %s' % (t['from'], t['date'])) for t in ti]] + for mt in patch.mailthread_set.order_by('-latestmessage'): + ti = sorted(_archivesAPI('/message-id.json/%s' % mt.messageid), key=lambda x: x['date'], reverse=True) + yield [mt.subject, + [('%s,%s' % (mt.messageid, t['msgid']),'From %s at %s' % (t['from'], t['date'])) for t in ti]] review_state_choices = ( - (0, 'Tested'), - (1, 'Passed'), + (0, 'Tested'), + (1, 'Passed'), ) def reviewfield(label): - return forms.MultipleChoiceField(choices=review_state_choices, label=label, widget=forms.CheckboxSelectMultiple, required=False) + return forms.MultipleChoiceField(choices=review_state_choices, label=label, widget=forms.CheckboxSelectMultiple, required=False) class CommentForm(forms.Form): - responseto = forms.ChoiceField(choices=[], required=True, label='In response to') - - # Specific checkbox fields for reviews - review_installcheck = reviewfield('make installcheck-world') - review_implements = reviewfield('Implements feature') - review_spec = reviewfield('Spec compliant') - review_doc = reviewfield('Documentation') - - message = forms.CharField(required=True, widget=forms.Textarea) - newstatus = forms.ChoiceField(choices=PatchOnCommitFest.OPEN_STATUS_CHOICES, label='New status') - - def __init__(self, patch, poc, is_review, *args, **kwargs): - super(CommentForm, self).__init__(*args, **kwargs) - self.is_review = is_review - - self.fields['responseto'].choices = _fetch_thread_choices(patch) - self.fields['newstatus'].initial = poc.status - if not is_review: - del self.fields['review_installcheck'] - del self.fields['review_implements'] - del self.fields['review_spec'] - del self.fields['review_doc'] - - def clean_responseto(self): - try: - (threadid, respid) = self.cleaned_data['responseto'].split(',') - self.thread = MailThread.objects.get(messageid=threadid) - self.respid = respid - except MailThread.DoesNotExist: - raise ValidationError('Selected thread appears to no longer exist') - except: - raise ValidationError('Invalid message selected') - return self.cleaned_data['responseto'] - - def clean(self): - if self.is_review: - for fn,f in self.fields.items(): - if fn.startswith('review_') and self.cleaned_data.has_key(fn): - if '1' in self.cleaned_data[fn] and not '0' in self.cleaned_data[fn]: - self.errors[fn] = (('Cannot pass a test without performing it!'),) - return self.cleaned_data + responseto = forms.ChoiceField(choices=[], required=True, label='In response to') + + # Specific checkbox fields for reviews + review_installcheck = reviewfield('make installcheck-world') + review_implements = reviewfield('Implements feature') + review_spec = reviewfield('Spec compliant') + review_doc = reviewfield('Documentation') + + message = forms.CharField(required=True, widget=forms.Textarea) + newstatus = forms.ChoiceField(choices=PatchOnCommitFest.OPEN_STATUS_CHOICES, label='New status') + + def __init__(self, patch, poc, is_review, *args, **kwargs): + super(CommentForm, self).__init__(*args, **kwargs) + self.is_review = is_review + + self.fields['responseto'].choices = _fetch_thread_choices(patch) + self.fields['newstatus'].initial = poc.status + if not is_review: + del self.fields['review_installcheck'] + del self.fields['review_implements'] + del self.fields['review_spec'] + del self.fields['review_doc'] + + def clean_responseto(self): + try: + (threadid, respid) = self.cleaned_data['responseto'].split(',') + self.thread = MailThread.objects.get(messageid=threadid) + self.respid = respid + except MailThread.DoesNotExist: + raise ValidationError('Selected thread appears to no longer exist') + except: + raise ValidationError('Invalid message selected') + return self.cleaned_data['responseto'] + + def clean(self): + if self.is_review: + for fn,f in self.fields.items(): + if fn.startswith('review_') and self.cleaned_data.has_key(fn): + if '1' in self.cleaned_data[fn] and not '0' in self.cleaned_data[fn]: + self.errors[fn] = (('Cannot pass a test without performing it!'),) + return self.cleaned_data class BulkEmailForm(forms.Form): - reviewers = forms.CharField(required=False, widget=HiddenInput()) - authors = forms.CharField(required=False, widget=HiddenInput()) - subject = forms.CharField(required=True) - body = forms.CharField(required=True, widget=forms.Textarea) - confirm = forms.BooleanField(required=True, label='Check to confirm sending') - - def __init__(self, *args, **kwargs): - super(BulkEmailForm, self).__init__(*args, **kwargs) + reviewers = forms.CharField(required=False, widget=HiddenInput()) + authors = forms.CharField(required=False, widget=HiddenInput()) + subject = forms.CharField(required=True) + body = forms.CharField(required=True, widget=forms.Textarea) + confirm = forms.BooleanField(required=True, label='Check to confirm sending') + + def __init__(self, *args, **kwargs): + super(BulkEmailForm, self).__init__(*args, **kwargs) diff --git a/pgcommitfest/commitfest/lookups.py b/pgcommitfest/commitfest/lookups.py index 74c08cc..1cb567f 100644 --- a/pgcommitfest/commitfest/lookups.py +++ b/pgcommitfest/commitfest/lookups.py @@ -6,20 +6,20 @@ from selectable.decorators import login_required @login_required class UserLookup(ModelLookup): - model = User - search_fields = ( - 'username__icontains', - 'first_name__icontains', - 'last_name__icontains', - ) - filters = {'is_active': True, } + model = User + search_fields = ( + 'username__icontains', + 'first_name__icontains', + 'last_name__icontains', + ) + filters = {'is_active': True, } - def get_item_value(self, item): - # Display for currently selected item - return u"%s (%s)" % (item.username, item.get_full_name()) + def get_item_value(self, item): + # Display for currently selected item + return u"%s (%s)" % (item.username, item.get_full_name()) - def get_item_label(self, item): - # Display for choice listings - return u"%s (%s)" % (item.username, item.get_full_name()) + def get_item_label(self, item): + # Display for choice listings + return u"%s (%s)" % (item.username, item.get_full_name()) registry.register(UserLookup) diff --git a/pgcommitfest/commitfest/management/commands/send_notifications.py b/pgcommitfest/commitfest/management/commands/send_notifications.py index 6a8fe42..be8cd90 100644 --- a/pgcommitfest/commitfest/management/commands/send_notifications.py +++ b/pgcommitfest/commitfest/management/commands/send_notifications.py @@ -9,38 +9,38 @@ from pgcommitfest.userprofile.models import UserProfile from pgcommitfest.mailqueue.util import send_template_mail class Command(BaseCommand): - help = "Send queued notifications" + help = "Send queued notifications" - def handle(self, *args, **options): - with transaction.atomic(): - # Django doesn't do proper group by in the ORM, so we have to - # build our own. - matches = {} - for n in PendingNotification.objects.all().order_by('user', 'history__patch__id', 'history__id'): - if not matches.has_key(n.user.id): - matches[n.user.id] = {'user': n.user, 'patches': {}} - if not matches[n.user.id]['patches'].has_key(n.history.patch.id): - matches[n.user.id]['patches'][n.history.patch.id] = {'patch': n.history.patch, 'entries': []} - matches[n.user.id]['patches'][n.history.patch.id]['entries'].append(n.history) - n.delete() + def handle(self, *args, **options): + with transaction.atomic(): + # Django doesn't do proper group by in the ORM, so we have to + # build our own. + matches = {} + for n in PendingNotification.objects.all().order_by('user', 'history__patch__id', 'history__id'): + if not matches.has_key(n.user.id): + matches[n.user.id] = {'user': n.user, 'patches': {}} + if not matches[n.user.id]['patches'].has_key(n.history.patch.id): + matches[n.user.id]['patches'][n.history.patch.id] = {'patch': n.history.patch, 'entries': []} + matches[n.user.id]['patches'][n.history.patch.id]['entries'].append(n.history) + n.delete() - # Ok, now let's build emails from this - for v in matches.values(): - user = v['user'] - email = user.email - try: - if user.userprofile and user.userprofile.notifyemail: - email = user.userprofile.notifyemail.email - except UserProfile.DoesNotExist: - pass + # Ok, now let's build emails from this + for v in matches.values(): + user = v['user'] + email = user.email + try: + if user.userprofile and user.userprofile.notifyemail: + email = user.userprofile.notifyemail.email + except UserProfile.DoesNotExist: + pass - send_template_mail(settings.NOTIFICATION_FROM, - None, - email, - "PostgreSQL commitfest updates", - 'mail/patch_notify.txt', - { - 'user': user, - 'patches': v['patches'], - }, - ) + send_template_mail(settings.NOTIFICATION_FROM, + None, + email, + "PostgreSQL commitfest updates", + 'mail/patch_notify.txt', + { + 'user': user, + 'patches': v['patches'], + }, + ) diff --git a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py index 346adf9..fa61fe3 100644 --- a/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py +++ b/pgcommitfest/commitfest/migrations/0003_withdrawn_status.py @@ -16,7 +16,7 @@ class Migration(migrations.Migration): name='status', field=models.IntegerField(default=1, choices=[(1, b'Needs review'), (2, b'Waiting on Author'), (3, b'Ready for Committer'), (4, b'Committed'), (5, b'Moved to next CF'), (6, b'Rejected'), (7, b'Returned with feedback'), (8, b'Withdrawn')]), ), - migrations.RunSQL(""" + migrations.RunSQL(""" INSERT INTO commitfest_patchstatus (status, statusstring, sortkey) VALUES (1,'Needs review',10), (2,'Waiting on Author',15), @@ -28,5 +28,5 @@ INSERT INTO commitfest_patchstatus (status, statusstring, sortkey) VALUES (8,'Withdrawn', 50) ON CONFLICT (status) DO UPDATE SET statusstring=excluded.statusstring, sortkey=excluded.sortkey; """), - migrations.RunSQL("DELETE FROM commitfest_patchstatus WHERE status < 1 OR status > 8"), + migrations.RunSQL("DELETE FROM commitfest_patchstatus WHERE status < 1 OR status > 8"), ] diff --git a/pgcommitfest/commitfest/models.py b/pgcommitfest/commitfest/models.py index 60dcaf4..1cc694e 100644 --- a/pgcommitfest/commitfest/models.py +++ b/pgcommitfest/commitfest/models.py @@ -11,310 +11,310 @@ from pgcommitfest.userprofile.models import UserProfile # need to extend from the user model, so just create a separate # class. class Committer(models.Model): - user = models.OneToOneField(User, null=False, blank=False, primary_key=True) - active = models.BooleanField(null=False, blank=False, default=True) + user = models.OneToOneField(User, null=False, blank=False, primary_key=True) + active = models.BooleanField(null=False, blank=False, default=True) - def __unicode__(self): - return unicode(self.user) + def __unicode__(self): + return unicode(self.user) - @property - def fullname(self): - return "%s %s (%s)" % (self.user.first_name, self.user.last_name, self.user.username) + @property + def fullname(self): + return "%s %s (%s)" % (self.user.first_name, self.user.last_name, self.user.username) - class Meta: - ordering = ('user__last_name', 'user__first_name') + class Meta: + ordering = ('user__last_name', 'user__first_name') class CommitFest(models.Model): - STATUS_FUTURE=1 - STATUS_OPEN=2 - STATUS_INPROGRESS=3 - STATUS_CLOSED=4 - _STATUS_CHOICES = ( - (STATUS_FUTURE, 'Future'), - (STATUS_OPEN, 'Open'), - (STATUS_INPROGRESS, 'In Progress'), - (STATUS_CLOSED, 'Closed'), - ) - name = models.CharField(max_length=100, blank=False, null=False, unique=True) - status = models.IntegerField(null=False, blank=False, default=1, choices=_STATUS_CHOICES) - startdate = models.DateField(blank=True, null=True) - enddate = models.DateField(blank=True, null=True) - - @property - def statusstring(self): - return [v for k,v in self._STATUS_CHOICES if k==self.status][0] - - @property - def periodstring(self): - if self.startdate and self.enddate: - return "{0} - {1}".format(self.startdate, self.enddate) - return "" - - @property - def title(self): - return "Commitfest %s" % self.name - - @property - def isopen(self): - return self.status == self.STATUS_OPEN - - def __unicode__(self): - return self.name - - class Meta: - verbose_name_plural='Commitfests' - ordering = ('-startdate',) + STATUS_FUTURE=1 + STATUS_OPEN=2 + STATUS_INPROGRESS=3 + STATUS_CLOSED=4 + _STATUS_CHOICES = ( + (STATUS_FUTURE, 'Future'), + (STATUS_OPEN, 'Open'), + (STATUS_INPROGRESS, 'In Progress'), + (STATUS_CLOSED, 'Closed'), + ) + name = models.CharField(max_length=100, blank=False, null=False, unique=True) + status = models.IntegerField(null=False, blank=False, default=1, choices=_STATUS_CHOICES) + startdate = models.DateField(blank=True, null=True) + enddate = models.DateField(blank=True, null=True) + + @property + def statusstring(self): + return [v for k,v in self._STATUS_CHOICES if k==self.status][0] + + @property + def periodstring(self): + if self.startdate and self.enddate: + return "{0} - {1}".format(self.startdate, self.enddate) + return "" + + @property + def title(self): + return "Commitfest %s" % self.name + + @property + def isopen(self): + return self.status == self.STATUS_OPEN + + def __unicode__(self): + return self.name + + class Meta: + verbose_name_plural='Commitfests' + ordering = ('-startdate',) class Topic(models.Model): - topic = models.CharField(max_length=100, blank=False, null=False) + topic = models.CharField(max_length=100, blank=False, null=False) - def __unicode__(self): - return self.topic + def __unicode__(self): + return self.topic class Patch(models.Model, DiffableModel): - name = models.CharField(max_length=500, blank=False, null=False, verbose_name='Description') - topic = models.ForeignKey(Topic, blank=False, null=False) - - # One patch can be in multiple commitfests, if it has history - commitfests = models.ManyToManyField(CommitFest, through='PatchOnCommitFest') - - # If there is a wiki page discussing this patch - wikilink = models.URLField(blank=True, null=False, default='') - - # If there is a git repo about this patch - gitlink = models.URLField(blank=True, null=False, default='') - - # Mailthreads are ManyToMany in the other direction - #mailthreads_set = ... - - authors = models.ManyToManyField(User, related_name='patch_author', blank=True) - reviewers = models.ManyToManyField(User, related_name='patch_reviewer', blank=True) - - committer = models.ForeignKey(Committer, blank=True, null=True) - - # Users to be notified when something happens - subscribers = models.ManyToManyField(User, related_name='patch_subscriber', blank=True) - - # Datestamps for tracking activity - created = models.DateTimeField(blank=False, null=False, auto_now_add=True) - modified = models.DateTimeField(blank=False, null=False) - - # Materialize the last time an email was sent on any of the threads - # that's attached to this message. - lastmail = models.DateTimeField(blank=True, null=True) - - map_manytomany_for_diff = { - 'authors': 'authors_string', - 'reviewers': 'reviewers_string', - } - # Some accessors - @property - def authors_string(self): - return ", ".join(["%s %s (%s)" % (a.first_name, a.last_name, a.username) for a in self.authors.all()]) - - @property - def reviewers_string(self): - return ", ".join(["%s %s (%s)" % (a.first_name, a.last_name, a.username) for a in self.reviewers.all()]) - - @property - def history(self): - # Need to wrap this in a function to make sure it calls - # select_related() and doesn't generate a bazillion queries - return self.patchhistory_set.select_related('by').all() - - def set_modified(self, newmod=None): - # Set the modified date to newmod, but only if that's newer than - # what's currently set. If newmod is not specified, use the - # current timestamp. - if not newmod: - newmod = datetime.now() - if not self.modified or newmod > self.modified: - self.modified = newmod - - def update_lastmail(self): - # Update the lastmail field, based on the newest email in any of - # the threads attached to it. - threads = list(self.mailthread_set.all()) - if len(threads) == 0: - self.lastmail = None - else: - self.lastmail = max(threads, key=lambda t:t.latestmessage).latestmessage - - def __unicode__(self): - return self.name - - class Meta: - verbose_name_plural = 'patches' + name = models.CharField(max_length=500, blank=False, null=False, verbose_name='Description') + topic = models.ForeignKey(Topic, blank=False, null=False) + + # One patch can be in multiple commitfests, if it has history + commitfests = models.ManyToManyField(CommitFest, through='PatchOnCommitFest') + + # If there is a wiki page discussing this patch + wikilink = models.URLField(blank=True, null=False, default='') + + # If there is a git repo about this patch + gitlink = models.URLField(blank=True, null=False, default='') + + # Mailthreads are ManyToMany in the other direction + #mailthreads_set = ... + + authors = models.ManyToManyField(User, related_name='patch_author', blank=True) + reviewers = models.ManyToManyField(User, related_name='patch_reviewer', blank=True) + + committer = models.ForeignKey(Committer, blank=True, null=True) + + # Users to be notified when something happens + subscribers = models.ManyToManyField(User, related_name='patch_subscriber', blank=True) + + # Datestamps for tracking activity + created = models.DateTimeField(blank=False, null=False, auto_now_add=True) + modified = models.DateTimeField(blank=False, null=False) + + # Materialize the last time an email was sent on any of the threads + # that's attached to this message. + lastmail = models.DateTimeField(blank=True, null=True) + + map_manytomany_for_diff = { + 'authors': 'authors_string', + 'reviewers': 'reviewers_string', + } + # Some accessors + @property + def authors_string(self): + return ", ".join(["%s %s (%s)" % (a.first_name, a.last_name, a.username) for a in self.authors.all()]) + + @property + def reviewers_string(self): + return ", ".join(["%s %s (%s)" % (a.first_name, a.last_name, a.username) for a in self.reviewers.all()]) + + @property + def history(self): + # Need to wrap this in a function to make sure it calls + # select_related() and doesn't generate a bazillion queries + return self.patchhistory_set.select_related('by').all() + + def set_modified(self, newmod=None): + # Set the modified date to newmod, but only if that's newer than + # what's currently set. If newmod is not specified, use the + # current timestamp. + if not newmod: + newmod = datetime.now() + if not self.modified or newmod > self.modified: + self.modified = newmod + + def update_lastmail(self): + # Update the lastmail field, based on the newest email in any of + # the threads attached to it. + threads = list(self.mailthread_set.all()) + if len(threads) == 0: + self.lastmail = None + else: + self.lastmail = max(threads, key=lambda t:t.latestmessage).latestmessage + + def __unicode__(self): + return self.name + + class Meta: + verbose_name_plural = 'patches' class PatchOnCommitFest(models.Model): - # NOTE! This is also matched by the commitfest_patchstatus table, - # but we hardcoded it in here simply for performance reasons since - # the data should be entirely static. (Yes, that's something we - # might re-evaluate in the future) - STATUS_REVIEW=1 - STATUS_AUTHOR=2 - STATUS_COMMITTER=3 - STATUS_COMMITTED=4 - STATUS_NEXT=5 - STATUS_REJECTED=6 - STATUS_RETURNED=7 - STATUS_WITHDRAWN=8 - _STATUS_CHOICES=( - (STATUS_REVIEW, 'Needs review'), - (STATUS_AUTHOR, 'Waiting on Author'), - (STATUS_COMMITTER, 'Ready for Committer'), - (STATUS_COMMITTED, 'Committed'), - (STATUS_NEXT, 'Moved to next CF'), - (STATUS_REJECTED, 'Rejected'), - (STATUS_RETURNED, 'Returned with feedback'), - (STATUS_WITHDRAWN, 'Withdrawn'), - ) - _STATUS_LABELS=( - (STATUS_REVIEW, 'default'), - (STATUS_AUTHOR, 'primary'), - (STATUS_COMMITTER, 'info'), - (STATUS_COMMITTED, 'success'), - (STATUS_NEXT, 'warning'), - (STATUS_REJECTED, 'danger'), - (STATUS_RETURNED, 'danger'), - (STATUS_WITHDRAWN, 'danger'), - ) - OPEN_STATUSES=[STATUS_REVIEW, STATUS_AUTHOR, STATUS_COMMITTER] - OPEN_STATUS_CHOICES=[x for x in _STATUS_CHOICES if x[0] in OPEN_STATUSES] - - patch = models.ForeignKey(Patch, blank=False, null=False) - commitfest = models.ForeignKey(CommitFest, blank=False, null=False) - enterdate = models.DateTimeField(blank=False, null=False) - leavedate = models.DateTimeField(blank=True, null=True) - - status = models.IntegerField(blank=False, null=False, default=STATUS_REVIEW, choices=_STATUS_CHOICES) - - @property - def is_closed(self): - return self.status not in self.OPEN_STATUSES - - @property - def statusstring(self): - return [v for k,v in self._STATUS_CHOICES if k==self.status][0] - - class Meta: - unique_together = (('patch', 'commitfest',),) - ordering = ('-commitfest__startdate', ) + # NOTE! This is also matched by the commitfest_patchstatus table, + # but we hardcoded it in here simply for performance reasons since + # the data should be entirely static. (Yes, that's something we + # might re-evaluate in the future) + STATUS_REVIEW=1 + STATUS_AUTHOR=2 + STATUS_COMMITTER=3 + STATUS_COMMITTED=4 + STATUS_NEXT=5 + STATUS_REJECTED=6 + STATUS_RETURNED=7 + STATUS_WITHDRAWN=8 + _STATUS_CHOICES=( + (STATUS_REVIEW, 'Needs review'), + (STATUS_AUTHOR, 'Waiting on Author'), + (STATUS_COMMITTER, 'Ready for Committer'), + (STATUS_COMMITTED, 'Committed'), + (STATUS_NEXT, 'Moved to next CF'), + (STATUS_REJECTED, 'Rejected'), + (STATUS_RETURNED, 'Returned with feedback'), + (STATUS_WITHDRAWN, 'Withdrawn'), + ) + _STATUS_LABELS=( + (STATUS_REVIEW, 'default'), + (STATUS_AUTHOR, 'primary'), + (STATUS_COMMITTER, 'info'), + (STATUS_COMMITTED, 'success'), + (STATUS_NEXT, 'warning'), + (STATUS_REJECTED, 'danger'), + (STATUS_RETURNED, 'danger'), + (STATUS_WITHDRAWN, 'danger'), + ) + OPEN_STATUSES=[STATUS_REVIEW, STATUS_AUTHOR, STATUS_COMMITTER] + OPEN_STATUS_CHOICES=[x for x in _STATUS_CHOICES if x[0] in OPEN_STATUSES] + + patch = models.ForeignKey(Patch, blank=False, null=False) + commitfest = models.ForeignKey(CommitFest, blank=False, null=False) + enterdate = models.DateTimeField(blank=False, null=False) + leavedate = models.DateTimeField(blank=True, null=True) + + status = models.IntegerField(blank=False, null=False, default=STATUS_REVIEW, choices=_STATUS_CHOICES) + + @property + def is_closed(self): + return self.status not in self.OPEN_STATUSES + + @property + def statusstring(self): + return [v for k,v in self._STATUS_CHOICES if k==self.status][0] + + class Meta: + unique_together = (('patch', 'commitfest',),) + ordering = ('-commitfest__startdate', ) class PatchHistory(models.Model): - patch = models.ForeignKey(Patch, blank=False, null=False) - date = models.DateTimeField(blank=False, null=False, auto_now_add=True) - by = models.ForeignKey(User, blank=False, null=False) - what = models.CharField(max_length=500, null=False, blank=False) - - @property - def by_string(self): - return "%s %s (%s)" % (self.by.first_name, self.by.last_name, self.by.username) - - def __unicode__(self): - return "%s - %s" % (self.patch.name, self.date) - - class Meta: - ordering = ('-date', ) - - def save_and_notify(self, prevcommitter=None, - prevreviewers=None, prevauthors=None): - # Save this model, and then trigger notifications if there are any. There are - # many different things that can trigger notifications, so try them all. - self.save() - - recipients = [] - recipients.extend(self.patch.subscribers.all()) - - # Current or previous committer wants all notifications - try: - if self.patch.committer and self.patch.committer.user.userprofile.notify_all_committer: - recipients.append(self.patch.committer.user) - except UserProfile.DoesNotExist: - pass - - try: - if prevcommitter and prevcommitter.user.userprofile.notify_all_committer: - recipients.append(prevcommitter.user) - except UserProfile.DoesNotExist: - pass - - # Current or previous reviewers wants all notifications - recipients.extend(self.patch.reviewers.filter(userprofile__notify_all_reviewer=True)) - if prevreviewers: - # prevreviewers is a list - recipients.extend(User.objects.filter(id__in=[p.id for p in prevreviewers], userprofile__notify_all_reviewer=True)) - - # Current or previous authors wants all notifications - recipients.extend(self.patch.authors.filter(userprofile__notify_all_author=True)) - - for u in set(recipients): - if u != self.by: # Don't notify for changes we make ourselves - PendingNotification(history=self, user=u).save() + patch = models.ForeignKey(Patch, blank=False, null=False) + date = models.DateTimeField(blank=False, null=False, auto_now_add=True) + by = models.ForeignKey(User, blank=False, null=False) + what = models.CharField(max_length=500, null=False, blank=False) + + @property + def by_string(self): + return "%s %s (%s)" % (self.by.first_name, self.by.last_name, self.by.username) + + def __unicode__(self): + return "%s - %s" % (self.patch.name, self.date) + + class Meta: + ordering = ('-date', ) + + def save_and_notify(self, prevcommitter=None, + prevreviewers=None, prevauthors=None): + # Save this model, and then trigger notifications if there are any. There are + # many different things that can trigger notifications, so try them all. + self.save() + + recipients = [] + recipients.extend(self.patch.subscribers.all()) + + # Current or previous committer wants all notifications + try: + if self.patch.committer and self.patch.committer.user.userprofile.notify_all_committer: + recipients.append(self.patch.committer.user) + except UserProfile.DoesNotExist: + pass + + try: + if prevcommitter and prevcommitter.user.userprofile.notify_all_committer: + recipients.append(prevcommitter.user) + except UserProfile.DoesNotExist: + pass + + # Current or previous reviewers wants all notifications + recipients.extend(self.patch.reviewers.filter(userprofile__notify_all_reviewer=True)) + if prevreviewers: + # prevreviewers is a list + recipients.extend(User.objects.filter(id__in=[p.id for p in prevreviewers], userprofile__notify_all_reviewer=True)) + + # Current or previous authors wants all notifications + recipients.extend(self.patch.authors.filter(userprofile__notify_all_author=True)) + + for u in set(recipients): + if u != self.by: # Don't notify for changes we make ourselves + PendingNotification(history=self, user=u).save() class MailThread(models.Model): - # This class tracks mail threads from the main postgresql.org - # mailinglist archives. For each thread, we store *one* messageid. - # Using this messageid we can always query the archives for more - # detailed information, which is done dynamically as the page - # is loaded. - # For threads in an active or future commitfest, we also poll - # the archives to fetch "updated entries" at (ir)regular intervals - # so we can keep track of when there was last a change on the - # thread in question. - messageid = models.CharField(max_length=1000, null=False, blank=False, unique=True) - patches = models.ManyToManyField(Patch, blank=False) - subject = models.CharField(max_length=500, null=False, blank=False) - firstmessage = models.DateTimeField(null=False, blank=False) - firstauthor = models.CharField(max_length=500, null=False, blank=False) - latestmessage = models.DateTimeField(null=False, blank=False) - latestauthor = models.CharField(max_length=500, null=False, blank=False) - latestsubject = models.CharField(max_length=500, null=False, blank=False) - latestmsgid = models.CharField(max_length=1000, null=False, blank=False) - - def __unicode__(self): - return self.subject - - class Meta: - ordering = ('firstmessage', ) + # This class tracks mail threads from the main postgresql.org + # mailinglist archives. For each thread, we store *one* messageid. + # Using this messageid we can always query the archives for more + # detailed information, which is done dynamically as the page + # is loaded. + # For threads in an active or future commitfest, we also poll + # the archives to fetch "updated entries" at (ir)regular intervals + # so we can keep track of when there was last a change on the + # thread in question. + messageid = models.CharField(max_length=1000, null=False, blank=False, unique=True) + patches = models.ManyToManyField(Patch, blank=False) + subject = models.CharField(max_length=500, null=False, blank=False) + firstmessage = models.DateTimeField(null=False, blank=False) + firstauthor = models.CharField(max_length=500, null=False, blank=False) + latestmessage = models.DateTimeField(null=False, blank=False) + latestauthor = models.CharField(max_length=500, null=False, blank=False) + latestsubject = models.CharField(max_length=500, null=False, blank=False) + latestmsgid = models.CharField(max_length=1000, null=False, blank=False) + + def __unicode__(self): + return self.subject + + class Meta: + ordering = ('firstmessage', ) class MailThreadAttachment(models.Model): - mailthread = models.ForeignKey(MailThread, null=False, blank=False) - messageid = models.CharField(max_length=1000, null=False, blank=False) - attachmentid = models.IntegerField(null=False, blank=False) - filename = models.CharField(max_length=1000, null=False, blank=True) - date = models.DateTimeField(null=False, blank=False) - author = models.CharField(max_length=500, null=False, blank=False) - ispatch = models.NullBooleanField() - - class Meta: - ordering = ('-date',) - unique_together = (('mailthread', 'messageid',), ) + mailthread = models.ForeignKey(MailThread, null=False, blank=False) + messageid = models.CharField(max_length=1000, null=False, blank=False) + attachmentid = models.IntegerField(null=False, blank=False) + filename = models.CharField(max_length=1000, null=False, blank=True) + date = models.DateTimeField(null=False, blank=False) + author = models.CharField(max_length=500, null=False, blank=False) + ispatch = models.NullBooleanField() + + class Meta: + ordering = ('-date',) + unique_together = (('mailthread', 'messageid',), ) class MailThreadAnnotation(models.Model): - mailthread = models.ForeignKey(MailThread, null=False, blank=False) - date = models.DateTimeField(null=False, blank=False, auto_now_add=True) - user = models.ForeignKey(User, null=False, blank=False) - msgid = models.CharField(max_length=1000, null=False, blank=False) - annotationtext = models.TextField(null=False, blank=False, max_length=2000) - mailsubject = models.CharField(max_length=500, null=False, blank=False) - maildate = models.DateTimeField(null=False, blank=False) - mailauthor = models.CharField(max_length=500, null=False, blank=False) - - @property - def user_string(self): - return "%s %s (%s)" % (self.user.first_name, self.user.last_name, self.user.username) - - class Meta: - ordering = ('date', ) + mailthread = models.ForeignKey(MailThread, null=False, blank=False) + date = models.DateTimeField(null=False, blank=False, auto_now_add=True) + user = models.ForeignKey(User, null=False, blank=False) + msgid = models.CharField(max_length=1000, null=False, blank=False) + annotationtext = models.TextField(null=False, blank=False, max_length=2000) + mailsubject = models.CharField(max_length=500, null=False, blank=False) + maildate = models.DateTimeField(null=False, blank=False) + mailauthor = models.CharField(max_length=500, null=False, blank=False) + + @property + def user_string(self): + return "%s %s (%s)" % (self.user.first_name, self.user.last_name, self.user.username) + + class Meta: + ordering = ('date', ) class PatchStatus(models.Model): - status = models.IntegerField(null=False, blank=False, primary_key=True) - statusstring = models.TextField(max_length=50, null=False, blank=False) - sortkey = models.IntegerField(null=False, blank=False, default=10) + status = models.IntegerField(null=False, blank=False, primary_key=True) + statusstring = models.TextField(max_length=50, null=False, blank=False) + sortkey = models.IntegerField(null=False, blank=False, default=10) class PendingNotification(models.Model): - history = models.ForeignKey(PatchHistory, blank=False, null=False) - user = models.ForeignKey(User, blank=False, null=False) + history = models.ForeignKey(PatchHistory, blank=False, null=False) + user = models.ForeignKey(User, blank=False, null=False) diff --git a/pgcommitfest/commitfest/reports.py b/pgcommitfest/commitfest/reports.py index f0d352e..4645856 100644 --- a/pgcommitfest/commitfest/reports.py +++ b/pgcommitfest/commitfest/reports.py @@ -8,12 +8,12 @@ from models import CommitFest @login_required def authorstats(request, cfid): - cf = get_object_or_404(CommitFest, pk=cfid) - if not request.user.is_staff: - raise Http404("Only CF Managers can do that.") + cf = get_object_or_404(CommitFest, pk=cfid) + if not request.user.is_staff: + raise Http404("Only CF Managers can do that.") - cursor = connection.cursor() - cursor.execute("""WITH patches(id,name) AS ( + cursor = connection.cursor() + cursor.execute("""WITH patches(id,name) AS ( SELECT p.id, name FROM commitfest_patch p INNER JOIN commitfest_patchoncommitfest poc ON poc.patch_id=p.id AND poc.commitfest_id=%(cid)s @@ -35,12 +35,12 @@ FROM (authors FULL OUTER JOIN reviewers ON authors.userid=reviewers.userid) INNER JOIN auth_user u ON u.id=COALESCE(authors.userid, reviewers.userid) ORDER BY last_name, first_name """, { - 'cid': cf.id, + 'cid': cf.id, }) - return render(request, 'report_authors.html', { - 'cf': cf, - 'report': cursor.fetchall(), - 'title': 'Author stats', - 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], - }) + return render(request, 'report_authors.html', { + 'cf': cf, + 'report': cursor.fetchall(), + 'title': 'Author stats', + 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], + }) diff --git a/pgcommitfest/commitfest/templatetags/commitfest.py b/pgcommitfest/commitfest/templatetags/commitfest.py index b8f68e4..bde7d07 100644 --- a/pgcommitfest/commitfest/templatetags/commitfest.py +++ b/pgcommitfest/commitfest/templatetags/commitfest.py @@ -8,36 +8,36 @@ register = template.Library() @register.filter(name='patchstatusstring') @stringfilter def patchstatusstring(value): - i = int(value) - return [v for k,v in PatchOnCommitFest._STATUS_CHOICES if k==i][0] + i = int(value) + return [v for k,v in PatchOnCommitFest._STATUS_CHOICES if k==i][0] @register.filter(name='patchstatuslabel') @stringfilter def patchstatuslabel(value): - i = int(value) - return [v for k,v in PatchOnCommitFest._STATUS_LABELS if k==i][0] + i = int(value) + return [v for k,v in PatchOnCommitFest._STATUS_LABELS if k==i][0] @register.filter(is_safe=True) def label_class(value, arg): - return value.label_tag(attrs={'class': arg}) + return value.label_tag(attrs={'class': arg}) @register.filter(is_safe=True) def field_class(value, arg): - return value.as_widget(attrs={"class": arg}) + return value.as_widget(attrs={"class": arg}) @register.filter(name='alertmap') @stringfilter def alertmap(value): - if value == 'error': - return 'alert-danger' - elif value == 'warning': - return 'alert-warning' - elif value == 'success': - return 'alert-success' - else: - return 'alert-info' + if value == 'error': + return 'alert-danger' + elif value == 'warning': + return 'alert-warning' + elif value == 'success': + return 'alert-success' + else: + return 'alert-info' @register.filter(name='hidemail') @stringfilter def hidemail(value): - return value.replace('@', ' at ') + return value.replace('@', ' at ') diff --git a/pgcommitfest/commitfest/util.py b/pgcommitfest/commitfest/util.py index ba09433..4698879 100644 --- a/pgcommitfest/commitfest/util.py +++ b/pgcommitfest/commitfest/util.py @@ -3,42 +3,42 @@ import django.db.models.fields.related class DiffableModel(object): - """ - Make it possible to diff a model. + """ + Make it possible to diff a model. """ - def __init__(self, *args, **kwargs): - super(DiffableModel, self).__init__(*args, **kwargs) - self.__initial = self._dict + def __init__(self, *args, **kwargs): + super(DiffableModel, self).__init__(*args, **kwargs) + self.__initial = self._dict - @property - def diff(self): - manytomanyfieldnames = [f.name for f in self._meta.many_to_many] - d1 = self.__initial - d2 = self._dict - diffs = dict([(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]) - # Foreign key lookups - for k,v in diffs.items(): - if type(self._meta.get_field(k)) is django.db.models.fields.related.ForeignKey: - # If it's a foreign key, look up the name again on ourselves. - # Since we only care about the *new* value, it's easy enough. - diffs[k] = (v[0], getattr(self, k)) - # Many to many lookups - if hasattr(self, 'map_manytomany_for_diff'): - for k,v in diffs.items(): - if k in manytomanyfieldnames and self.map_manytomany_for_diff.has_key(k): - # Try to show the display name instead here - newvalue = getattr(self, self.map_manytomany_for_diff[k]) - diffs[k] = (v[0], newvalue) - return diffs + @property + def diff(self): + manytomanyfieldnames = [f.name for f in self._meta.many_to_many] + d1 = self.__initial + d2 = self._dict + diffs = dict([(k, (v, d2[k])) for k, v in d1.items() if v != d2[k]]) + # Foreign key lookups + for k,v in diffs.items(): + if type(self._meta.get_field(k)) is django.db.models.fields.related.ForeignKey: + # If it's a foreign key, look up the name again on ourselves. + # Since we only care about the *new* value, it's easy enough. + diffs[k] = (v[0], getattr(self, k)) + # Many to many lookups + if hasattr(self, 'map_manytomany_for_diff'): + for k,v in diffs.items(): + if k in manytomanyfieldnames and self.map_manytomany_for_diff.has_key(k): + # Try to show the display name instead here + newvalue = getattr(self, self.map_manytomany_for_diff[k]) + diffs[k] = (v[0], newvalue) + return diffs - def save(self, *args, **kwargs): - super(DiffableModel, self).save(*args, **kwargs) - self.__initial = self._dict + def save(self, *args, **kwargs): + super(DiffableModel, self).save(*args, **kwargs) + self.__initial = self._dict - @property - def _dict(self): - fields = [field.name for field in self._meta.fields] - fields.extend([field.name for field in self._meta.many_to_many]) - return model_to_dict(self, fields=fields) + @property + def _dict(self): + fields = [field.name for field in self._meta.fields] + fields.extend([field.name for field in self._meta.many_to_many]) + return model_to_dict(self, fields=fields) diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index 5f68895..705b172 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -27,170 +27,170 @@ from ajax import doAttachThread, refresh_single_thread 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) + 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(request, 'home.html', { - 'commitfests': commitfests, - 'opencf': opencf, - 'inprogresscf': inprogresscf, - 'title': 'Commitfests', - 'header_activity': 'Activity log', - 'header_activity_link': '/activity/', - }) + return render(request, 'home.html', { + 'commitfests': commitfests, + 'opencf': opencf, + 'inprogresscf': inprogresscf, + 'title': 'Commitfests', + 'header_activity': 'Activity log', + 'header_activity_link': '/activity/', + }) 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(request, '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, - }) + # 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(request, '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, + }) def redir(request, what): - if what == 'open': - cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_OPEN)) - elif what == 'inprogress': - cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_INPROGRESS)) - else: - raise Http404() - - if len(cfs) == 0: - messages.warning(request, "No {0} commitfests exist, redirecting to startpage.".format(what)) - return HttpResponseRedirect("/") - if len(cfs) != 1: - messages.warning(request, "More than one {0} commitfest exists, redirecting to startpage instead.".format(what)) - return HttpResponseRedirect("/") - - return HttpResponseRedirect("/%s/" % cfs[0].id) + if what == 'open': + cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_OPEN)) + elif what == 'inprogress': + cfs = list(CommitFest.objects.filter(status=CommitFest.STATUS_INPROGRESS)) + else: + raise Http404() + + if len(cfs) == 0: + messages.warning(request, "No {0} commitfests exist, redirecting to startpage.".format(what)) + return HttpResponseRedirect("/") + if len(cfs) != 1: + messages.warning(request, "More than one {0} commitfest exists, redirecting to startpage instead.".format(what)) + return HttpResponseRedirect("/") + + return HttpResponseRedirect("/%s/" % cfs[0].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 - whereclauses = [] - whereparams = {} - if request.GET.has_key('status') and request.GET['status'] != "-1": - try: - whereparams['status'] = int(request.GET['status']) - whereclauses.append("poc.status=%(status)s") - except ValueError: - # int() failed -- so just ignore this filter - pass - - if request.GET.has_key('author') and request.GET['author'] != "-1": - if request.GET['author'] == '-2': - whereclauses.append("NOT EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id)") - 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)) - whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s)") - whereparams['self'] = request.user.id - else: - try: - whereparams['author'] = int(request.GET['author']) - whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(author)s)") - except ValueError: - # int() failed -- so just ignore this filter - pass - - if request.GET.has_key('reviewer') and request.GET['reviewer'] != "-1": - if request.GET['reviewer'] == '-2': - whereclauses.append("NOT EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id)") - 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)) - whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id AND cpr.user_id=%(self)s)") - whereparams['self'] = request.user.id - else: - try: - whereparams['reviewer'] = int(request.GET['reviewer']) - whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id AND cpr.user_id=%(reviewer)s)") - except ValueError: - # int() failed -- so just ignore this filter - pass - - if request.GET.has_key('text') and request.GET['text'] != '': - whereclauses.append("p.name ILIKE '%%' || %(txt)s || '%%'") - whereparams['txt'] = request.GET['text'] - - has_filter = len(whereclauses) > 0 - - # Figure out custom ordering - if request.GET.has_key('sortkey') and request.GET['sortkey']!='': - try: - sortkey=int(request.GET['sortkey']) - except ValueError: - sortkey=0 - - if sortkey==1: - orderby_str = 'modified, created' - elif sortkey==2: - orderby_str = 'lastmail, created' - elif sortkey==3: - orderby_str = 'num_cfs DESC, modified, created' - else: - orderby_str = 'p.id' - sortkey=0 - else: - orderby_str = 'topic, created' - 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) - - if whereclauses: - where_str = 'AND ({0})'.format(' AND '.join(whereclauses)) - else: - where_str = '' - params = { - 'cid': cf.id, - 'openstatuses': PatchOnCommitFest.OPEN_STATUSES, - } - params.update(whereparams) - - # Let's not overload the poor django ORM - curs = connection.cursor() - curs.execute("""SELECT p.id, p.name, poc.status, p.created, p.modified, p.lastmail, committer.username AS committer, t.topic, + # Find ourselves + cf = get_object_or_404(CommitFest, pk=cfid) + + # Build a dynamic filter based on the filtering options entered + whereclauses = [] + whereparams = {} + if request.GET.has_key('status') and request.GET['status'] != "-1": + try: + whereparams['status'] = int(request.GET['status']) + whereclauses.append("poc.status=%(status)s") + except ValueError: + # int() failed -- so just ignore this filter + pass + + if request.GET.has_key('author') and request.GET['author'] != "-1": + if request.GET['author'] == '-2': + whereclauses.append("NOT EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id)") + 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)) + whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(self)s)") + whereparams['self'] = request.user.id + else: + try: + whereparams['author'] = int(request.GET['author']) + whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_authors cpa WHERE cpa.patch_id=p.id AND cpa.user_id=%(author)s)") + except ValueError: + # int() failed -- so just ignore this filter + pass + + if request.GET.has_key('reviewer') and request.GET['reviewer'] != "-1": + if request.GET['reviewer'] == '-2': + whereclauses.append("NOT EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id)") + 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)) + whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id AND cpr.user_id=%(self)s)") + whereparams['self'] = request.user.id + else: + try: + whereparams['reviewer'] = int(request.GET['reviewer']) + whereclauses.append("EXISTS (SELECT 1 FROM commitfest_patch_reviewers cpr WHERE cpr.patch_id=p.id AND cpr.user_id=%(reviewer)s)") + except ValueError: + # int() failed -- so just ignore this filter + pass + + if request.GET.has_key('text') and request.GET['text'] != '': + whereclauses.append("p.name ILIKE '%%' || %(txt)s || '%%'") + whereparams['txt'] = request.GET['text'] + + has_filter = len(whereclauses) > 0 + + # Figure out custom ordering + if request.GET.has_key('sortkey') and request.GET['sortkey']!='': + try: + sortkey=int(request.GET['sortkey']) + except ValueError: + sortkey=0 + + if sortkey==1: + orderby_str = 'modified, created' + elif sortkey==2: + orderby_str = 'lastmail, created' + elif sortkey==3: + orderby_str = 'num_cfs DESC, modified, created' + else: + orderby_str = 'p.id' + sortkey=0 + else: + orderby_str = 'topic, created' + 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) + + if whereclauses: + where_str = 'AND ({0})'.format(' AND '.join(whereclauses)) + else: + where_str = '' + params = { + 'cid': cf.id, + 'openstatuses': PatchOnCommitFest.OPEN_STATUSES, + } + params.update(whereparams) + + # Let's not overload the poor django ORM + curs = connection.cursor() + curs.execute("""SELECT p.id, p.name, poc.status, p.created, p.modified, p.lastmail, committer.username AS committer, t.topic, (poc.status=ANY(%(openstatuses)s)) AS is_open, (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=p.id) AS author_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=p.id) AS reviewer_names, @@ -202,533 +202,533 @@ LEFT JOIN auth_user committer ON committer.id=p.committer_id WHERE poc.commitfest_id=%(cid)s {0} GROUP BY p.id, poc.id, committer.id, t.id ORDER BY is_open DESC, {1}""".format(where_str, orderby_str), params) - patches = [dict(zip([col[0] for col in curs.description], row)) for row in curs.fetchall()] - - # 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(request, '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/', - }) + patches = [dict(zip([col[0] for col in curs.description], row)) for row in curs.fetchall()] + + # 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(request, '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/', + }) def global_search(request): - if not request.GET.has_key('searchterm'): - return HttpResponseRedirect('/') - searchterm = request.GET['searchterm'] + if not request.GET.has_key('searchterm'): + return HttpResponseRedirect('/') + searchterm = request.GET['searchterm'] - patches = Patch.objects.select_related().filter(name__icontains=searchterm).order_by('created',) + patches = Patch.objects.select_related().filter(name__icontains=searchterm).order_by('created',) - return render(request, 'patchsearch.html', { - 'patches': patches, - 'title': 'Patch search results', - }) + return render(request, 'patchsearch.html', { + 'patches': patches, + 'title': 'Patch search results', + }) 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() - is_subscribed = patch.subscribers.filter(id=request.user.id).exists() - else: - is_committer = False - is_this_committer = False - is_reviewer = False - is_subscribed = False - - return render(request, 'patch.html', { - 'cf': cf, - 'patch': patch, - 'patch_commitfests': patch_commitfests, - 'is_committer': is_committer, - 'is_this_committer': is_this_committer, - 'is_reviewer': is_reviewer, - 'is_subscribed': is_subscribed, - 'committers': committers, - 'attachnow': request.GET.has_key('attachthreadnow'), - 'title': patch.name, - 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], - }) + 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() + is_subscribed = patch.subscribers.filter(id=request.user.id).exists() + else: + is_committer = False + is_this_committer = False + is_reviewer = False + is_subscribed = False + + return render(request, 'patch.html', { + 'cf': cf, + 'patch': patch, + 'patch_commitfests': patch_commitfests, + 'is_committer': is_committer, + 'is_this_committer': is_this_committer, + 'is_reviewer': is_reviewer, + 'is_subscribed': is_subscribed, + 'committers': committers, + 'attachnow': request.GET.has_key('attachthreadnow'), + 'title': patch.name, + 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], + }) @login_required @transaction.atomic def patchform(request, cfid, patchid): - cf = get_object_or_404(CommitFest, pk=cfid) - patch = get_object_or_404(Patch, pk=patchid, commitfests=cf) - - prevreviewers = list(patch.reviewers.all()) - prevauthors = list(patch.authors.all()) - prevcommitter = patch.committer - - 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_and_notify(prevcommitter=prevcommitter, prevreviewers=prevreviewers, prevauthors=prevauthors) - 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(request, '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)}], - }) + cf = get_object_or_404(CommitFest, pk=cfid) + patch = get_object_or_404(Patch, pk=patchid, commitfests=cf) + + prevreviewers = list(patch.reviewers.all()) + prevauthors = list(patch.authors.all()) + prevcommitter = patch.committer + + 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_and_notify(prevcommitter=prevcommitter, prevreviewers=prevreviewers, prevauthors=prevauthors) + 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(request, '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)}], + }) @login_required @transaction.atomic 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.',)) - # In this case, we have created a patch - delete it. This causes a agp in id's, but it should - # not happen very often. If we successfully attached to it, we will have already returned. - patch.delete() - else: - form = NewPatchForm() - - return render(request, 'base_form.html', { - 'form': form, - 'title': 'New patch', - 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], - 'savebutton': 'Create patch', - 'threadbrowse': True, - }) + 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.',)) + # In this case, we have created a patch - delete it. This causes a agp in id's, but it should + # not happen very often. If we successfully attached to it, we will have already returned. + patch.delete() + else: + form = NewPatchForm() + + return render(request, 'base_form.html', { + 'form': form, + 'title': 'New patch', + 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], + 'savebutton': 'Create patch', + 'threadbrowse': True, + }) def _review_status_string(reviewstatus): - if '0' in reviewstatus: - if '1' in reviewstatus: - return "tested, passed" - else: - return "tested, failed" - else: - return "not tested" + if '0' in reviewstatus: + if '1' in reviewstatus: + return "tested, passed" + else: + return "tested, failed" + else: + return "not tested" @login_required @transaction.atomic 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: - # We allow modification of patches in closed CFs *only* if it's the - # last CF that the patch is part of. If it's part of another CF, that - # is later than this one, tell the user to go there instead. - lastcf = PatchOnCommitFest.objects.filter(patch=patch).order_by('-commitfest__startdate')[0] - if poc != lastcf: - 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_and_notify() - 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'] = UserWrapper(request.user).encoded_email_header - - # CC the authors of a patch, if there are any - authors = list(patch.authors.all()) - if len(authors): - msg['Cc'] = ", ".join([UserWrapper(a).encoded_email_header for a in authors]) - - 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') - - uw = UserWrapper(request.user) - msgstring = msg.as_string() - send_mail(uw.email, settings.HACKERS_EMAIL, msgstring) - for a in authors: - # Actually send a copy directly to the author. Just setting the Cc field doesn't - # make it deliver the email... - send_mail(uw.email, UserWrapper(a).email, msgstring) - - 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 %s, and will be sent within a few minutes." % (settings.HACKERS_EMAIL)) - - 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(request, '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!
Please ensure that the email settings for your domain (DKIM, SPF) allow emails from external sources.' % (settings.HACKERS_EMAIL, UserWrapper(request.user).email), - 'savebutton': 'Send %s' % 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: + # We allow modification of patches in closed CFs *only* if it's the + # last CF that the patch is part of. If it's part of another CF, that + # is later than this one, tell the user to go there instead. + lastcf = PatchOnCommitFest.objects.filter(patch=patch).order_by('-commitfest__startdate')[0] + if poc != lastcf: + 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_and_notify() + 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'] = UserWrapper(request.user).encoded_email_header + + # CC the authors of a patch, if there are any + authors = list(patch.authors.all()) + if len(authors): + msg['Cc'] = ", ".join([UserWrapper(a).encoded_email_header for a in authors]) + + 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') + + uw = UserWrapper(request.user) + msgstring = msg.as_string() + send_mail(uw.email, settings.HACKERS_EMAIL, msgstring) + for a in authors: + # Actually send a copy directly to the author. Just setting the Cc field doesn't + # make it deliver the email... + send_mail(uw.email, UserWrapper(a).email, msgstring) + + 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 %s, and will be sent within a few minutes." % (settings.HACKERS_EMAIL)) + + 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(request, '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!
Please ensure that the email settings for your domain (DKIM, SPF) allow emails from external sources.' % (settings.HACKERS_EMAIL, UserWrapper(request.user).email), + 'savebutton': 'Send %s' % what, + }) @login_required @transaction.atomic 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: - # We allow modification of patches in closed CFs *only* if it's the - # last CF that the patch is part of. If it's part of another CF, that - # is later than this one, tell the user to go there instead. - lastcf = PatchOnCommitFest.objects.filter(patch__id=patchid).order_by('-commitfest__startdate')[0] - if poc != lastcf: - 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('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - - 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_and_notify() - - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + poc = get_object_or_404(PatchOnCommitFest.objects.select_related(), commitfest__id=cfid, patch__id=patchid) + + if poc.is_closed: + # We allow modification of patches in closed CFs *only* if it's the + # last CF that the patch is part of. If it's part of another CF, that + # is later than this one, tell the user to go there instead. + lastcf = PatchOnCommitFest.objects.filter(patch__id=patchid).order_by('-commitfest__startdate')[0] + if poc != lastcf: + 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('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + + 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_and_notify() + + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) @login_required @transaction.atomic 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: - # We allow modification of patches in closed CFs *only* if it's the - # last CF that the patch is part of. If it's part of another CF, that - # is later than this one, tell the user to go there instead. - lastcf = PatchOnCommitFest.objects.filter(patch__id=patchid).order_by('-commitfest__startdate')[0] - if poc != lastcf: - 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('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - - 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 == 'withdrawn': - poc.status = PatchOnCommitFest.STATUS_WITHDRAWN - elif status == 'feedback': - poc.status = PatchOnCommitFest.STATUS_RETURNED - elif status == 'next': - # Only some patch statuses can actually be moved. - if poc.status in (PatchOnCommitFest.STATUS_AUTHOR, - PatchOnCommitFest.STATUS_COMMITTED, - PatchOnCommitFest.STATUS_NEXT, - PatchOnCommitFest.STATUS_RETURNED, - PatchOnCommitFest.STATUS_REJECTED): - # Can't be moved! - messages.error(request, "A patch in status {0} cannot be moved to next commitfest.".format(poc.statusstring)) - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - elif poc.status in (PatchOnCommitFest.STATUS_REVIEW, - PatchOnCommitFest.STATUS_COMMITTER): - # This one can be moved - pass - else: - messages.error(request, "Invalid existing patch status") - - oldstatus = poc.status - - poc.status = PatchOnCommitFest.STATUS_NEXT - # 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: - messages.error(request,"No open and no future commitfest exists!") - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - elif len(newcf) != 1: - messages.error(request, "No open and multiple future commitfests exist!") - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - elif len(newcf) != 1: - messages.error(request, "Multiple open commitfests exists!") - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - 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: - messages.error(request, "Cannot move patch to the same commitfest, and no future commitfests exist!") - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - elif len(newcf) != 1: - messages.error(request, "Cannot move patch to the same commitfest, and multiple future commitfests exist!") - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) - # Create a mapping to the new commitfest that we are bouncing - # this patch to. - newpoc = PatchOnCommitFest(patch=poc.patch, - commitfest=newcf[0], - status=oldstatus, - 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! - prevcommitter = poc.patch.committer - poc.patch.committer = committer - PatchHistory(patch=poc.patch, by=request.user, what='Changed committer to %s' % committer).save_and_notify(prevcommitter=prevcommitter) - 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_and_notify() - - return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + poc = get_object_or_404(PatchOnCommitFest.objects.select_related(), commitfest__id=cfid, patch__id=patchid) + + if poc.is_closed: + # We allow modification of patches in closed CFs *only* if it's the + # last CF that the patch is part of. If it's part of another CF, that + # is later than this one, tell the user to go there instead. + lastcf = PatchOnCommitFest.objects.filter(patch__id=patchid).order_by('-commitfest__startdate')[0] + if poc != lastcf: + 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('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + + 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 == 'withdrawn': + poc.status = PatchOnCommitFest.STATUS_WITHDRAWN + elif status == 'feedback': + poc.status = PatchOnCommitFest.STATUS_RETURNED + elif status == 'next': + # Only some patch statuses can actually be moved. + if poc.status in (PatchOnCommitFest.STATUS_AUTHOR, + PatchOnCommitFest.STATUS_COMMITTED, + PatchOnCommitFest.STATUS_NEXT, + PatchOnCommitFest.STATUS_RETURNED, + PatchOnCommitFest.STATUS_REJECTED): + # Can't be moved! + messages.error(request, "A patch in status {0} cannot be moved to next commitfest.".format(poc.statusstring)) + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + elif poc.status in (PatchOnCommitFest.STATUS_REVIEW, + PatchOnCommitFest.STATUS_COMMITTER): + # This one can be moved + pass + else: + messages.error(request, "Invalid existing patch status") + + oldstatus = poc.status + + poc.status = PatchOnCommitFest.STATUS_NEXT + # 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: + messages.error(request,"No open and no future commitfest exists!") + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + elif len(newcf) != 1: + messages.error(request, "No open and multiple future commitfests exist!") + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + elif len(newcf) != 1: + messages.error(request, "Multiple open commitfests exists!") + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + 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: + messages.error(request, "Cannot move patch to the same commitfest, and no future commitfests exist!") + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + elif len(newcf) != 1: + messages.error(request, "Cannot move patch to the same commitfest, and multiple future commitfests exist!") + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) + # Create a mapping to the new commitfest that we are bouncing + # this patch to. + newpoc = PatchOnCommitFest(patch=poc.patch, + commitfest=newcf[0], + status=oldstatus, + 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! + prevcommitter = poc.patch.committer + poc.patch.committer = committer + PatchHistory(patch=poc.patch, by=request.user, what='Changed committer to %s' % committer).save_and_notify(prevcommitter=prevcommitter) + 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_and_notify() + + return HttpResponseRedirect('/%s/%s/' % (poc.commitfest.id, poc.patch.id)) @login_required @transaction.atomic def reviewer(request, cfid, patchid, status): - get_object_or_404(CommitFest, pk=cfid) - patch = get_object_or_404(Patch, pk=patchid) + get_object_or_404(CommitFest, pk=cfid) + patch = get_object_or_404(Patch, pk=patchid) - is_reviewer = request.user in patch.reviewers.all() + 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_and_notify() - 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_and_notify() - return HttpResponseRedirect('../../') + 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_and_notify() + 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_and_notify() + return HttpResponseRedirect('../../') @login_required @transaction.atomic 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 - - prevcommitter = 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_and_notify(prevcommitter=prevcommitter) - 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_and_notify(prevcommitter=prevcommitter) - patch.save() - return HttpResponseRedirect('../../') + 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 + + prevcommitter = 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_and_notify(prevcommitter=prevcommitter) + 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_and_notify(prevcommitter=prevcommitter) + patch.save() + return HttpResponseRedirect('../../') @login_required @transaction.atomic def subscribe(request, cfid, patchid, sub): - get_object_or_404(CommitFest, pk=cfid) - patch = get_object_or_404(Patch, pk=patchid) - - if sub == 'un': - patch.subscribers.remove(request.user) - messages.info(request, "You have been unsubscribed from updates on this patch") - else: - patch.subscribers.add(request.user) - messages.info(request, "You have been subscribed to updates on this patch") - patch.save() - return HttpResponseRedirect("../") + get_object_or_404(CommitFest, pk=cfid) + patch = get_object_or_404(Patch, pk=patchid) + + if sub == 'un': + patch.subscribers.remove(request.user) + messages.info(request, "You have been unsubscribed from updates on this patch") + else: + patch.subscribers.add(request.user) + messages.info(request, "You have been subscribed to updates on this patch") + patch.save() + return HttpResponseRedirect("../") @login_required @transaction.atomic 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(request, 'base_form.html', { - 'cf': cf, - 'form': form, - 'title': 'Send email', - 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], - 'savebutton': 'Send email', - }) + 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(request, 'base_form.html', { + 'cf': cf, + 'form': form, + 'title': 'Send email', + 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk},], + 'savebutton': 'Send email', + }) @csrf_exempt def thread_notify(request): - if request.method != 'POST': - return HttpResponseForbidden("Invalid method") - - j = json.loads(request.body) - if j['apikey'] != settings.ARCHIVES_APIKEY: - return HttpResponseForbidden("Invalid API key") - - for m in j['messageids']: - try: - t = MailThread.objects.get(messageid=m) - refresh_single_thread(t) - except Exception, e: - # Just ignore it, we'll check again later - pass - - return HttpResponse(status=200) + if request.method != 'POST': + return HttpResponseForbidden("Invalid method") + + j = json.loads(request.body) + if j['apikey'] != settings.ARCHIVES_APIKEY: + return HttpResponseForbidden("Invalid API key") + + for m in j['messageids']: + try: + t = MailThread.objects.get(messageid=m) + refresh_single_thread(t) + except Exception, e: + # Just ignore it, we'll check again later + pass + + return HttpResponse(status=200) diff --git a/pgcommitfest/commitfest/widgets.py b/pgcommitfest/commitfest/widgets.py index 0475001..ee2df3d 100644 --- a/pgcommitfest/commitfest/widgets.py +++ b/pgcommitfest/commitfest/widgets.py @@ -2,8 +2,8 @@ from django.forms import TextInput from django.utils.safestring import mark_safe class ThreadPickWidget(TextInput): - def render(self, name, value, attrs=None): - attrs['class'] += ' threadpick-input' - html = super(ThreadPickWidget, self).render(name, value, attrs) - html = html + ' ' % name - return mark_safe(html) + def render(self, name, value, attrs=None): + attrs['class'] += ' threadpick-input' + html = super(ThreadPickWidget, self).render(name, value, attrs) + html = html + ' ' % name + return mark_safe(html) diff --git a/pgcommitfest/mailqueue/models.py b/pgcommitfest/mailqueue/models.py index f2e7d19..f75ca37 100644 --- a/pgcommitfest/mailqueue/models.py +++ b/pgcommitfest/mailqueue/models.py @@ -1,11 +1,11 @@ from django.db import models class QueuedMail(models.Model): - sender = models.EmailField(max_length=100, null=False, blank=False) - receiver = models.EmailField(max_length=100, null=False, blank=False) - # We store the raw MIME message, so if there are any attachments or - # anything, we just push them right in there! - fullmsg = models.TextField(null=False, blank=False) + sender = models.EmailField(max_length=100, null=False, blank=False) + receiver = models.EmailField(max_length=100, null=False, blank=False) + # We store the raw MIME message, so if there are any attachments or + # anything, we just push them right in there! + fullmsg = models.TextField(null=False, blank=False) - def __unicode__(self): - return "%s: %s -> %s" % (self.pk, self.sender, self.receiver) + def __unicode__(self): + return "%s: %s -> %s" % (self.pk, self.sender, self.receiver) diff --git a/pgcommitfest/mailqueue/util.py b/pgcommitfest/mailqueue/util.py index 9d58750..9abea53 100644 --- a/pgcommitfest/mailqueue/util.py +++ b/pgcommitfest/mailqueue/util.py @@ -9,37 +9,37 @@ from email import encoders from models import QueuedMail def send_simple_mail(sender, receiver, subject, msgtxt, sending_username, attachments=None): - # attachment format, each is a tuple of (name, mimetype,contents) - # content should already be base64 encoded - msg = MIMEMultipart() - msg['Subject'] = subject - msg['To'] = receiver - msg['From'] = sender - msg['Date'] = formatdate(localtime=True) - msg['User-Agent'] = 'pgcommitfest' - if sending_username: - msg['X-cfsender'] = sending_username - - msg.attach(MIMEText(msgtxt, _charset='utf-8')) - - if attachments: - for filename, contenttype, content in attachments: - main,sub = contenttype.split('/') - part = MIMENonMultipart(main,sub) - part.set_payload(content) - part.add_header('Content-Disposition', 'attachment; filename="%s"' % filename) - encoders.encode_base64(part) - msg.attach(part) - - - # Just write it to the queue, so it will be transactionally rolled back - QueuedMail(sender=sender, receiver=receiver, fullmsg=msg.as_string()).save() + # attachment format, each is a tuple of (name, mimetype,contents) + # content should already be base64 encoded + msg = MIMEMultipart() + msg['Subject'] = subject + msg['To'] = receiver + msg['From'] = sender + msg['Date'] = formatdate(localtime=True) + msg['User-Agent'] = 'pgcommitfest' + if sending_username: + msg['X-cfsender'] = sending_username + + msg.attach(MIMEText(msgtxt, _charset='utf-8')) + + if attachments: + for filename, contenttype, content in attachments: + main,sub = contenttype.split('/') + part = MIMENonMultipart(main,sub) + part.set_payload(content) + part.add_header('Content-Disposition', 'attachment; filename="%s"' % filename) + encoders.encode_base64(part) + msg.attach(part) + + + # Just write it to the queue, so it will be transactionally rolled back + QueuedMail(sender=sender, receiver=receiver, fullmsg=msg.as_string()).save() def send_mail(sender, receiver, fullmsg): - # Send an email, prepared as the full MIME encoded mail already - QueuedMail(sender=sender, receiver=receiver, fullmsg=fullmsg).save() + # Send an email, prepared as the full MIME encoded mail already + QueuedMail(sender=sender, receiver=receiver, fullmsg=fullmsg).save() def send_template_mail(sender, senderaccountname, receiver, subject, templatename, templateattr={}, usergenerated=False): - send_simple_mail(sender, receiver, subject, - get_template(templatename).render(templateattr), - senderaccountname) + send_simple_mail(sender, receiver, subject, + get_template(templatename).render(templateattr), + senderaccountname) diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py index 1c1a860..575d90f 100644 --- a/pgcommitfest/settings.py +++ b/pgcommitfest/settings.py @@ -6,7 +6,7 @@ TEMPLATE_DEBUG = DEBUG ALLOWED_HOSTS = ['*'] ADMINS = ( - ('webmaster@postgresql.org', 'webmaster@postgresql.org'), + ('webmaster@postgresql.org', 'webmaster@postgresql.org'), ) MANAGERS = ADMINS @@ -98,19 +98,19 @@ MIDDLEWARE_CLASSES = ( ROOT_URLCONF = 'pgcommitfest.urls' TEMPLATES = [{ - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': ['global_templates'], - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', - ], - 'loaders': [ - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', - ], - }, + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': ['global_templates'], + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + 'loaders': [ + 'django.template.loaders.filesystem.Loader', + 'django.template.loaders.app_directories.Loader', + ], + }, }] INSTALLED_APPS = ( @@ -123,10 +123,10 @@ INSTALLED_APPS = ( 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', - 'pgcommitfest.selectable', - 'pgcommitfest.commitfest', - 'pgcommitfest.mailqueue', - 'pgcommitfest.userprofile', + 'pgcommitfest.selectable', + 'pgcommitfest.commitfest', + 'pgcommitfest.mailqueue', + 'pgcommitfest.userprofile', ) AUTHENTICATION_BACKENDS = ( diff --git a/pgcommitfest/userprofile/admin.py b/pgcommitfest/userprofile/admin.py index c99922b..4b73d1f 100644 --- a/pgcommitfest/userprofile/admin.py +++ b/pgcommitfest/userprofile/admin.py @@ -3,6 +3,6 @@ from django.contrib import admin from models import UserProfile class UserProfileAdmin(admin.ModelAdmin): - list_display = ('user', ) + list_display = ('user', ) admin.site.register(UserProfile, UserProfileAdmin) diff --git a/pgcommitfest/userprofile/forms.py b/pgcommitfest/userprofile/forms.py index b177ef1..400a068 100644 --- a/pgcommitfest/userprofile/forms.py +++ b/pgcommitfest/userprofile/forms.py @@ -4,40 +4,40 @@ from django.contrib.auth.models import User from models import UserProfile, UserExtraEmail class UserProfileForm(forms.ModelForm): - class Meta: - model = UserProfile - exclude = ('user', ) + class Meta: + model = UserProfile + exclude = ('user', ) - def __init__(self, user, *args, **kwargs): - super(UserProfileForm, self).__init__(*args, **kwargs) - self.user = user + def __init__(self, user, *args, **kwargs): + super(UserProfileForm, self).__init__(*args, **kwargs) + self.user = user - self.fields['selectedemail'].empty_label=self.user.email - self.fields['selectedemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) - self.fields['notifyemail'].empty_label=self.user.email - self.fields['notifyemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) + self.fields['selectedemail'].empty_label=self.user.email + self.fields['selectedemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) + self.fields['notifyemail'].empty_label=self.user.email + self.fields['notifyemail'].queryset=UserExtraEmail.objects.filter(user=self.user, confirmed=True) class MailForm(forms.Form): - email = forms.EmailField() - email2 = forms.EmailField(label="Repeat email") + email = forms.EmailField() + email2 = forms.EmailField(label="Repeat email") - def clean_email(self): - email = self.cleaned_data['email'] + def clean_email(self): + email = self.cleaned_data['email'] - if User.objects.filter(email=email).exists(): - raise forms.ValidationError("This email is already in use by another account") + if User.objects.filter(email=email).exists(): + raise forms.ValidationError("This email is already in use by another account") - return email + return email - def clean_email2(self): - # If the primary email checker had an exception, the data will be gone - # from the cleaned_data structure - if not self.cleaned_data.has_key('email'): - return self.cleaned_data['email2'] - email1 = self.cleaned_data['email'] - email2 = self.cleaned_data['email2'] + def clean_email2(self): + # If the primary email checker had an exception, the data will be gone + # from the cleaned_data structure + if not self.cleaned_data.has_key('email'): + return self.cleaned_data['email2'] + email1 = self.cleaned_data['email'] + email2 = self.cleaned_data['email2'] - if email1 != email2: - raise forms.ValidationError("Email addresses don't match") + if email1 != email2: + raise forms.ValidationError("Email addresses don't match") - return email2 + return email2 diff --git a/pgcommitfest/userprofile/models.py b/pgcommitfest/userprofile/models.py index f5e5887..367e0bd 100644 --- a/pgcommitfest/userprofile/models.py +++ b/pgcommitfest/userprofile/models.py @@ -2,30 +2,30 @@ from django.db import models from django.contrib.auth.models import User class UserExtraEmail(models.Model): - user = models.ForeignKey(User, null=False, blank=False, db_index=True) - email = models.EmailField(max_length=100, null=False, blank=False, unique=True) - confirmed = models.BooleanField(null=False, blank=False, default=False) - token = models.CharField(max_length=100, null=False, blank=True) - tokensent = models.DateTimeField(null=False, blank=False) + user = models.ForeignKey(User, null=False, blank=False, db_index=True) + email = models.EmailField(max_length=100, null=False, blank=False, unique=True) + confirmed = models.BooleanField(null=False, blank=False, default=False) + token = models.CharField(max_length=100, null=False, blank=True) + tokensent = models.DateTimeField(null=False, blank=False) - def __unicode__(self): - return self.email + def __unicode__(self): + return self.email - class Meta: - ordering = ('user', 'email') - unique_together = (('user', 'email'),) + class Meta: + ordering = ('user', 'email') + unique_together = (('user', 'email'),) class UserProfile(models.Model): - user = models.OneToOneField(User, null=False, blank=False) - selectedemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, - verbose_name='Sender email') - notifyemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, - verbose_name='Notifications sent to', - related_name='notifier') - notify_all_author = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where author") - notify_all_reviewer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where reviewer") - notify_all_committer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where committer") + user = models.OneToOneField(User, null=False, blank=False) + selectedemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, + verbose_name='Sender email') + notifyemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, + verbose_name='Notifications sent to', + related_name='notifier') + notify_all_author = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where author") + notify_all_reviewer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where reviewer") + notify_all_committer = models.BooleanField(null=False, blank=False, default=False, verbose_name="Notify on all where committer") - def __unicode__(self): - return unicode(self.user) + def __unicode__(self): + return unicode(self.user) diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py index 89b4e28..bd2bcc8 100644 --- a/pgcommitfest/userprofile/util.py +++ b/pgcommitfest/userprofile/util.py @@ -6,34 +6,34 @@ from email.header import Header from models import UserProfile def generate_random_token(): - """ - Generate a random token of 64 characters. This token will be - generated using a strong random number, and then hex encoded to make - sure all characters are safe to put in emails and URLs. - """ - s = SHA256.new() - r = Random.new() - s.update(r.read(250)) - return s.hexdigest() + """ + Generate a random token of 64 characters. This token will be + generated using a strong random number, and then hex encoded to make + sure all characters are safe to put in emails and URLs. + """ + s = SHA256.new() + r = Random.new() + s.update(r.read(250)) + return s.hexdigest() class UserWrapper(object): - def __init__(self, user): - self.user = user + def __init__(self, user): + self.user = user - @property - def email(self): - try: - up = UserProfile.objects.get(user=self.user) - if up.selectedemail and up.selectedemail.confirmed: - return up.selectedemail.email - else: - return self.user.email - except UserProfile.DoesNotExist: - return self.user.email + @property + def email(self): + try: + up = UserProfile.objects.get(user=self.user) + if up.selectedemail and up.selectedemail.confirmed: + return up.selectedemail.email + else: + return self.user.email + except UserProfile.DoesNotExist: + return self.user.email - @property - def encoded_email_header(self): - return formataddr(( - str(Header(u"%s %s" % (self.user.first_name, self.user.last_name), 'utf-8')), - self.email)) + @property + def encoded_email_header(self): + return formataddr(( + str(Header(u"%s %s" % (self.user.first_name, self.user.last_name), 'utf-8')), + self.email)) diff --git a/pgcommitfest/userprofile/views.py b/pgcommitfest/userprofile/views.py index ced36b7..62e09ea 100644 --- a/pgcommitfest/userprofile/views.py +++ b/pgcommitfest/userprofile/views.py @@ -17,83 +17,83 @@ from util import generate_random_token @login_required @transaction.atomic def userprofile(request): - (profile, created) = UserProfile.objects.get_or_create(user=request.user) - form = mailform = None + (profile, created) = UserProfile.objects.get_or_create(user=request.user) + form = mailform = None - if request.method == 'POST': - if request.POST['submit'] == 'Save': - form = UserProfileForm(request.user, request.POST, instance=profile) - if form.is_valid(): - form.save() - messages.add_message(request, messages.INFO, "User profile saved.") - return HttpResponseRedirect('.') - elif request.POST['submit'] == 'Add email': - mailform = MailForm(request.POST) - if mailform.is_valid(): - m = UserExtraEmail(user=request.user, - email=mailform.cleaned_data['email'], - confirmed=False, - token=generate_random_token(), - tokensent=datetime.now()) - m.save() - send_template_mail(settings.NOTIFICATION_FROM, - request.user.username, - m.email, - 'Your email address for commitfest.postgresql.org', - 'extra_email_mail.txt', - {'token': m.token, 'user': m.user}) - messages.info(request, "A confirmation token has been sent to %s" % m.email) - return HttpResponseRedirect('.') - else: - messages.error(request, "Invalid submit button pressed! Nothing saved.") - return HttpResponseRedirect('.') + if request.method == 'POST': + if request.POST['submit'] == 'Save': + form = UserProfileForm(request.user, request.POST, instance=profile) + if form.is_valid(): + form.save() + messages.add_message(request, messages.INFO, "User profile saved.") + return HttpResponseRedirect('.') + elif request.POST['submit'] == 'Add email': + mailform = MailForm(request.POST) + if mailform.is_valid(): + m = UserExtraEmail(user=request.user, + email=mailform.cleaned_data['email'], + confirmed=False, + token=generate_random_token(), + tokensent=datetime.now()) + m.save() + send_template_mail(settings.NOTIFICATION_FROM, + request.user.username, + m.email, + 'Your email address for commitfest.postgresql.org', + 'extra_email_mail.txt', + {'token': m.token, 'user': m.user}) + messages.info(request, "A confirmation token has been sent to %s" % m.email) + return HttpResponseRedirect('.') + else: + messages.error(request, "Invalid submit button pressed! Nothing saved.") + return HttpResponseRedirect('.') - if not form: - form = UserProfileForm(request.user, instance=profile) - if not mailform: - mailform = MailForm() + if not form: + form = UserProfileForm(request.user, instance=profile) + if not mailform: + mailform = MailForm() - extramails = UserExtraEmail.objects.filter(user=request.user) + extramails = UserExtraEmail.objects.filter(user=request.user) - return render(request, 'userprofileform.html', { - 'form': form, - 'extramails': extramails, - 'mailform': mailform, - }) + return render(request, 'userprofileform.html', { + 'form': form, + 'extramails': extramails, + 'mailform': mailform, + }) @login_required @transaction.atomic def deletemail(request): - try: - id = int(request.META['QUERY_STRING']) - except ValueError: - messages.error(request, "Invalid format of id in query string") - return HttpResponseRedirect('../') + try: + id = int(request.META['QUERY_STRING']) + except ValueError: + messages.error(request, "Invalid format of id in query string") + return HttpResponseRedirect('../') - try: - e = UserExtraEmail.objects.get(user=request.user, id=id) - except UserExtraEmail.DoesNotExist: - messages.error(request, "Specified email address does not exist on this user") - return HttpResponseRedirect('../') + try: + e = UserExtraEmail.objects.get(user=request.user, id=id) + except UserExtraEmail.DoesNotExist: + messages.error(request, "Specified email address does not exist on this user") + return HttpResponseRedirect('../') - messages.info(request, "Email address %s deleted." % e.email) - e.delete() - return HttpResponseRedirect('../') + messages.info(request, "Email address %s deleted." % e.email) + e.delete() + return HttpResponseRedirect('../') @login_required @transaction.atomic def confirmemail(request, tokenhash): - try: - e = UserExtraEmail.objects.get(user=request.user, token=tokenhash) - if e.confirmed: - messages.warning(request, "This email address has already been confirmed.") - else: - # Ok, it's not confirmed. So let's do that now - e.confirmed = True - e.token = '' - e.save() - messages.info(request, "Email address %s added to profile." % e.email) - except UserExtraEmail.DoesNotExist: - messages.error(request, "Token %s was not found for your user. It may be because it has already been used?" % tokenhash) + try: + e = UserExtraEmail.objects.get(user=request.user, token=tokenhash) + if e.confirmed: + messages.warning(request, "This email address has already been confirmed.") + else: + # Ok, it's not confirmed. So let's do that now + e.confirmed = True + e.token = '' + e.save() + messages.info(request, "Email address %s added to profile." % e.email) + except UserExtraEmail.DoesNotExist: + messages.error(request, "Token %s was not found for your user. It may be because it has already been used?" % tokenhash) - return HttpResponseRedirect("../../") + return HttpResponseRedirect("../../") -- 2.39.5