diff options
author | Magnus Hagander | 2015-01-22 14:04:09 +0000 |
---|---|---|
committer | Magnus Hagander | 2015-01-22 14:04:09 +0000 |
commit | 0c58317302c0eeea57dcbafa28b5150b82eba3de (patch) | |
tree | 2f02f27f4780e89b1bd9918ceb7f6f1477328ad8 | |
parent | c80b7e1d58f3bac19a6df99ddf3d4bfc50e6a2ee (diff) |
Implement support for secondary email addresses
Each user can add a secondary email (well, more than one) and then pick
one of those when sending email.
Addresses are validated by sending a token to the newly added address,
with a link to click to confirm it. Only a fully confirmed address can
actually be used.
-rw-r--r-- | pgcommitfest/commitfest/templates/base.html | 2 | ||||
-rw-r--r-- | pgcommitfest/commitfest/views.py | 11 | ||||
-rw-r--r-- | pgcommitfest/mailqueue/util.py | 8 | ||||
-rw-r--r-- | pgcommitfest/settings.py | 4 | ||||
-rw-r--r-- | pgcommitfest/urls.py | 5 | ||||
-rw-r--r-- | pgcommitfest/userprofile/__init__.py | 0 | ||||
-rw-r--r-- | pgcommitfest/userprofile/forms.py | 41 | ||||
-rw-r--r-- | pgcommitfest/userprofile/models.py | 25 | ||||
-rw-r--r-- | pgcommitfest/userprofile/templates/extra_email_mail.txt | 7 | ||||
-rw-r--r-- | pgcommitfest/userprofile/templates/userprofileform.html | 113 | ||||
-rw-r--r-- | pgcommitfest/userprofile/util.py | 31 | ||||
-rw-r--r-- | pgcommitfest/userprofile/views.py | 98 |
12 files changed, 339 insertions, 6 deletions
diff --git a/pgcommitfest/commitfest/templates/base.html b/pgcommitfest/commitfest/templates/base.html index e60f3a9..a22b1ee 100644 --- a/pgcommitfest/commitfest/templates/base.html +++ b/pgcommitfest/commitfest/templates/base.html @@ -20,7 +20,7 @@ <li class="active">{{title}}</li> <li class="pull-right active"> {%if user.is_authenticated%} - Logged in as {{user}} (<a href="/https/git.postgresql.org/account/logout/">log out</a>{%if user.is_staff%} or access <a href="/https/git.postgresql.org/admin/">administration</a>{%endif%}) + Logged in as {{user}} (<a href="/https/git.postgresql.org/account/profile/">edit profile</a> | <a href="/https/git.postgresql.org/account/logout/">log out</a>{%if user.is_staff%} | <a href="/https/git.postgresql.org/admin/">administration</a>{%endif%}) {%else%} <a href="/https/git.postgresql.org/account/login/?next={{request.path}}">Log in</a> {%endif%} diff --git a/pgcommitfest/commitfest/views.py b/pgcommitfest/commitfest/views.py index f9ec991..a57000a 100644 --- a/pgcommitfest/commitfest/views.py +++ b/pgcommitfest/commitfest/views.py @@ -14,6 +14,7 @@ from email.mime.text import MIMEText from email.utils import formatdate, make_msgid from mailqueue.util import send_mail, send_simple_mail +from userprofile.util import UserWrapper from models import CommitFest, Patch, PatchOnCommitFest, PatchHistory, Committer from forms import PatchForm, NewPatchForm, CommentForm, CommitFestFilterForm @@ -333,7 +334,7 @@ def comment(request, cfid, patchid, what): msg['Subject'] = 'Re: %s' % form.thread.subject msg['To'] = settings.HACKERS_EMAIL - msg['From'] = "%s %s <%s>" % (request.user.first_name, request.user.last_name, request.user.email) + msg['From'] = "%s %s <%s>" % (request.user.first_name, request.user.last_name, UserWrapper(request.user).email) msg['Date'] = formatdate(localtime=True) msg['User-Agent'] = 'pgcommitfest' msg['X-cfsender'] = request.user.username @@ -344,7 +345,7 @@ def comment(request, cfid, patchid, what): msg['References'] = '<%s> <%s>' % (form.thread.messageid, form.respid) msg['Message-ID'] = make_msgid('pgcf') - send_mail(request.user.email, settings.HACKERS_EMAIL, msg) + send_mail(UserWrapper(request.user).email, settings.HACKERS_EMAIL, msg) PatchHistory(patch=patch, by=request.user, what='Posted %s with messageid %s' % (what, msg['Message-ID'])).save() @@ -366,7 +367,7 @@ def comment(request, cfid, patchid, what): 'breadcrumbs': [{'title': cf.title, 'href': '/%s/' % cf.pk}, {'title': 'View patch', 'href': '/%s/%s/' % (cf.pk, patch.pk)}], 'title': "Add %s" % what, - 'note': '<b>Note!</b> This form will generate an email to the public mailinglist <i>pgsql-hackers</i>, with sender set to %s!' % (request.user.email), + 'note': '<b>Note!</b> This form will generate an email to the public mailinglist <i>pgsql-hackers</i>, with sender set to %s!' % (UserWrapper(request.user).email), 'savebutton': 'Send %s' % what, }, context_instance=RequestContext(request)) @@ -522,7 +523,7 @@ def send_email(request, cfid): recipients = User.objects.filter(q).distinct() for r in recipients: - send_simple_mail(request.user.email, r.email, form.cleaned_data['subject'], form.cleaned_data['body'], request.user.username) + 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: @@ -543,7 +544,7 @@ def send_email(request, cfid): 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" % request.user.email) + 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) diff --git a/pgcommitfest/mailqueue/util.py b/pgcommitfest/mailqueue/util.py index 60afd5d..38e1145 100644 --- a/pgcommitfest/mailqueue/util.py +++ b/pgcommitfest/mailqueue/util.py @@ -1,3 +1,6 @@ +from django.template import Context +from django.template.loader import get_template + from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart from email.mime.nonmultipart import MIMENonMultipart @@ -35,3 +38,8 @@ def send_simple_mail(sender, receiver, subject, msgtxt, sending_username, attach 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() + +def send_template_mail(sender, receiver, subject, templatename, templateattr={}, usergenerated=False): + send_simple_mail(sender, receiver, subject, + get_template(templatename).render(Context(templateattr)), + '__internal') diff --git a/pgcommitfest/settings.py b/pgcommitfest/settings.py index 5ef0bd4..f33c71b 100644 --- a/pgcommitfest/settings.py +++ b/pgcommitfest/settings.py @@ -128,6 +128,7 @@ INSTALLED_APPS = ( 'selectable', 'commitfest', 'mailqueue', + 'userprofile', ) # A sample logging configuration. The only tangible logging @@ -166,6 +167,9 @@ ARCHIVES_HOST="archives.postgresql.org" # Host: header to send # Email address to pgsql-hackers. Set to something local to test maybe? HACKERS_EMAIL="pgsql-hackers-testing@localhost" +# Email address for outgoing system messages +NOTIFICATION_FROM="[email protected]" + # Load local settings overrides try: from local_settings import * diff --git a/pgcommitfest/urls.py b/pgcommitfest/urls.py index a715c71..46e0332 100644 --- a/pgcommitfest/urls.py +++ b/pgcommitfest/urls.py @@ -31,6 +31,11 @@ urlpatterns = patterns('', (r'^(?:account/)?logout/?$', 'auth.logout'), (r'^auth_receive/$', 'auth.auth_receive'), + # Account management + (r'^account/profile/$', 'userprofile.views.userprofile'), + (r'^account/profile/delmail/$', 'userprofile.views.deletemail'), + (r'^account/profile/confirm/([0-9a-f]+)/$', 'userprofile.views.confirmemail'), + # Examples: # url(r'^$', 'pgcommitfest.views.home', name='home'), # url(r'^pgcommitfest/', include('pgcommitfest.foo.urls')), diff --git a/pgcommitfest/userprofile/__init__.py b/pgcommitfest/userprofile/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/pgcommitfest/userprofile/__init__.py diff --git a/pgcommitfest/userprofile/forms.py b/pgcommitfest/userprofile/forms.py new file mode 100644 index 0000000..b7db7ad --- /dev/null +++ b/pgcommitfest/userprofile/forms.py @@ -0,0 +1,41 @@ +from django import forms +from django.contrib.auth.models import User + +from models import UserProfile, UserExtraEmail + +class UserProfileForm(forms.ModelForm): + class Meta: + model = UserProfile + exclude = ('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) + +class MailForm(forms.Form): + email = forms.EmailField() + email2 = forms.EmailField(label="Repeat 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") + + 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'] + + if email1 != email2: + raise forms.ValidationError("Email addresses don't match") + + return email2 diff --git a/pgcommitfest/userprofile/models.py b/pgcommitfest/userprofile/models.py new file mode 100644 index 0000000..c5a982c --- /dev/null +++ b/pgcommitfest/userprofile/models.py @@ -0,0 +1,25 @@ +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) + + def __unicode__(self): + return self.email + + class Meta: + ordering = ('user', 'email') + unique_together = (('user', 'email'),) + + +class UserProfile(models.Model): + user = models.ForeignKey(User, null=False, blank=False) + selectedemail = models.ForeignKey(UserExtraEmail, null=True, blank=True, + verbose_name='Sender email') + + def __unicode__(self): + return unicode(self.user) diff --git a/pgcommitfest/userprofile/templates/extra_email_mail.txt b/pgcommitfest/userprofile/templates/extra_email_mail.txt new file mode 100644 index 0000000..34c93cc --- /dev/null +++ b/pgcommitfest/userprofile/templates/extra_email_mail.txt @@ -0,0 +1,7 @@ +Somebody, probably you, has registered this email address as a secondary +address for the account {{user.username}} on commitfest.postgresql.org. + +To confirm this addition, please click on the following link: + +https://commitfest.postgresql.org/account/profile/confirm/{{token}}/ + diff --git a/pgcommitfest/userprofile/templates/userprofileform.html b/pgcommitfest/userprofile/templates/userprofileform.html new file mode 100644 index 0000000..4764fad --- /dev/null +++ b/pgcommitfest/userprofile/templates/userprofileform.html @@ -0,0 +1,113 @@ +{%extends "base.html"%} +{%load commitfest%} + +{%block contents%} +<style> +.form-horizontal div.form-group { + margin-bottom: 10px; +} +div.form-group div.controls ul { + list-style-type: none; + margin: 0px; + padding: 0px; +} +div.form-group div.controls ul li { + display: inline; +} +div.form-group div.controls ul li label { + display: inline; + font-weight: normal; + vertical-align:middle; +} +div.form-group div.controls ul li label input { + display: inline; + vertical-align:middle; +} +div.form-group div.controls input[type='checkbox'] { + width: 10px; +} + +div.controls ul.selectable-deck li.selectable-deck-item { + display: block; +} + +div.controls ul.selectable-deck li.selectable-deck-item a.selectable-deck-remove { + float: none; + margin-left: 10px; +} + +div.form-group div.controls input.threadpick-input { + width: 80%; + display: inline; +} +</style> +<form class="form-horizontal {{extraformclass}}" method="POST" action=".">{%csrf_token%} +{%if form.errors%} + <div class="alert">Please correct the errors below, and re-submit the form.</div> +{%endif%} +{%if form.non_field_errors%} + <div class="alert alert-danger">{{form.non_field_errors}}</div> +{%endif%} + {%for field in form%} + {%if not field.is_hidden%} + <div class="form-group"> + {{field|label_class:"control-label col-lg-1"}} + <div class="col-lg-11 controls"> + {%if field.errors %} + {%for e in field.errors%} + <div class="alert alert-danger">{{e}}</div> + {%endfor%} + {%endif%} +{{field|field_class:"form-control"}} +{%if field.help_text%}<br/>{{field.help_text|safe}}{%endif%}</div> + </div> + {%else%} +{{field}} + {%endif%} +{%endfor%} + <div class="form-group"> + <div class="col-lg-12"> + <div class="control"><input type="submit" class="btn btn-default" name="submit" value="Save"></div> + </div> + </div> +</form> + +<h2>Extra email addresses</h2> +<p> +The following extra email addresses are registered for your account: +</p> +<ul> +{%for e in extramails%} + <li>{{e.email}}{%if not e.confirmed%} (<i>Pending confirmation</i>){%endif%} <a href="delmail/?{{e.id}}">delete</a></li> +{%endfor%} +</ul> + +<h3>Add email</h3> +<form class="form-horizontal" method="post" action=".">{%csrf_token%} +{%if mailform.errors%} + <div class="alert">Please correct the errors below, and re-submit the form.</div> +{%endif%} +{%if mailform.non_field_errors%} + <div class="alert alert-danger">{{mailform.non_field_errors}}</div> +{%endif%} + {%for field in mailform%} + <div class="form-group"> + {{field|label_class:"control-label col-lg-1"}} + <div class="col-lg-11 controls"> + {%if field.errors %} + {%for e in field.errors%} + <div class="alert alert-danger">{{e}}</div> + {%endfor%} + {%endif%} +{{field|field_class:"form-control"}} +{%if field.help_text%}<br/>{{field.help_text|safe}}{%endif%}</div> + </div> +{%endfor%} + + <div class="form-group"> + <div class="col-lg-12"> + <div class="control"><input type="submit" class="btn btn-default" name="submit" value="Add email"></div> + </div> + </div> +</form> +{%endblock%} diff --git a/pgcommitfest/userprofile/util.py b/pgcommitfest/userprofile/util.py new file mode 100644 index 0000000..af50caf --- /dev/null +++ b/pgcommitfest/userprofile/util.py @@ -0,0 +1,31 @@ +from Crypto.Hash import SHA256 +from Crypto import Random + +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() + + +class UserWrapper(object): + 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 diff --git a/pgcommitfest/userprofile/views.py b/pgcommitfest/userprofile/views.py new file mode 100644 index 0000000..0d19a4f --- /dev/null +++ b/pgcommitfest/userprofile/views.py @@ -0,0 +1,98 @@ +from django.shortcuts import render_to_response +from django.http import HttpResponseRedirect +from django.template import RequestContext +from django.db import transaction +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.conf import settings + +from datetime import datetime + +from mailqueue.util import send_template_mail + +from models import UserProfile, UserExtraEmail +from forms import UserProfileForm, MailForm +from util import generate_random_token + +@login_required [email protected]_on_success +def userprofile(request): + (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, + 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() + + extramails = UserExtraEmail.objects.filter(user=request.user) + + return render_to_response('userprofileform.html', { + 'form': form, + 'extramails': extramails, + 'mailform': mailform, + }, context_instance=RequestContext(request)) + +@login_required [email protected]_on_success +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: + 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('../') + +@login_required [email protected]_on_success +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) + + return HttpResponseRedirect("../../") |