Add email confirmation step so that we can (in the future) send team and event notifications to a user. Fixes #29

This commit is contained in:
Michael Hall 2018-02-26 11:53:50 -05:00
parent 30e2aed4af
commit ba69749cc5
16 changed files with 294 additions and 13 deletions

View file

@ -0,0 +1,31 @@
# Generated by Django 2.0 on 2018-02-26 15:32
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('accounts', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='EmailConfirmation',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('email', models.CharField(max_length=256)),
('key', models.CharField(max_length=256)),
('expires', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AddField(
model_name='account',
name='is_email_confirmed',
field=models.BooleanField(default=False),
),
]

View file

@ -2,6 +2,8 @@ from django.db import models
from django.contrib.sites.models import Site
from django.contrib.auth.models import User, Group, AnonymousUser
from django.utils.translation import ugettext_lazy as _
from django.utils.crypto import get_random_string
from django.conf import settings
import pytz
import datetime
@ -12,12 +14,35 @@ class Account(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
acctname = models.CharField(_("Account Name"), max_length=150, blank=True)
is_email_confirmed = models.BooleanField(default=False)
badges = models.ManyToManyField('Badge', through='BadgeGrant')
class Meta:
ordering = ('user__username',)
def new_confirmation_request(self):
valid_for = getattr(settings, 'EMAIL_CONFIRMAION_EXPIRATION_DAYS', 5)
confirmation_key=get_random_string(length=32)
return EmailConfirmation.objects.create(
user=self.user,
email=self.user.email,
key=confirmation_key,
expires=datetime.datetime.now()+datetime.timedelta(days=valid_for)
)
def confirm_email(self, confirmation_key):
try:
confirmation_request = EmailConfirmation.objects.get(user=self.user, email=self.user.email, key=confirmation_key, expires__gt=datetime.datetime.now())
if confirmation_request is not None:
self.is_email_confirmed = True
self.save()
confirmation_request.delete()
return True
except Exception as e:
print(e)
return False
def __str__(self):
try:
if self.acctname:
@ -50,6 +75,12 @@ def _getAnonAccount(self):
User.account = property(_getUserAccount)
AnonymousUser.account = property(_getAnonAccount)
class EmailConfirmation(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
email = models.CharField(max_length=256)
key = models.CharField(max_length=256)
expires = models.DateTimeField()
class Badge(models.Model):
name = models.CharField(_('Badge Name'), max_length=64, blank=False, null=False)
img_url = models.URLField(_('Badge Image'), blank=False, null=False)

View file

@ -215,7 +215,18 @@ class UserForm(forms.ModelForm):
class UserProfileForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['realname', 'avatar']
fields = ['realname', 'avatar', 'send_notifications']
labels = {
'send_notifications': _('Send me notification emails'),
}
class SendNotificationsForm(forms.ModelForm):
class Meta:
model = UserProfile
fields = ['send_notifications']
labels = {
'send_notifications': _('Send me notification emails'),
}
class SearchForm(forms.Form):
city = forms.IntegerField(required=False, widget=Lookup(source='/api/cities/', label='name'))

View file

@ -0,0 +1,18 @@
# Generated by Django 2.0 on 2018-02-26 15:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0009_auto_20180224_0556'),
]
operations = [
migrations.AddField(
model_name='userprofile',
name='send_notifications',
field=models.BooleanField(default=True, verbose_name='Send notification emails'),
),
]

View file

@ -13,7 +13,7 @@ class UserProfile(models.Model):
" Store profile information about a user "
user = models.OneToOneField(User, on_delete=models.CASCADE)
realname = models.CharField(_("Real Name"), max_length=150, blank=True)
realname = models.CharField(verbose_name=_("Real Name"), max_length=150, blank=True)
tz = models.CharField(max_length=32, verbose_name=_('Timezone'), default='UTC', choices=[(tz, tz) for tz in pytz.all_timezones], blank=False, null=False, help_text=_('The most commonly used timezone for this User.'))
avatar = models.URLField(verbose_name=_("Photo Image"), max_length=150, blank=True, null=True)
@ -21,6 +21,8 @@ class UserProfile(models.Model):
twitter = models.CharField(verbose_name=_('Twitter Name'), max_length=32, blank=True, null=True)
facebook = models.URLField(verbose_name=_('Facebook URL'), max_length=32, blank=True, null=True)
send_notifications = models.BooleanField(verbose_name=_('Send notification emails'), default=True)
class Meta:
ordering = ('user__username',)

View file

@ -0,0 +1,29 @@
{% load static %}
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Email from GetTogether.Community</title>
{% block meta %}{% endblock %}
<!-- Bootstrap core CSS -->
<link rel="stylesheet" href="https://gettogether.community{% static 'css/bootstrap/css/bootstrap.min.css' %}">
{%block styles %}{% endblock %}
</head>
<body>
<nav class="navbar navbar-toggleable-md navbar-inverse bg-inverse fixed-top">
<a class="navbar-brand" href="/">GetTogether</a>
</nav>
<main role="main" class="container">
{% block content %}{% endblock %}
</main>
</html>

View file

@ -0,0 +1,13 @@
{% extends "get_together/emails/base.html" %}
{% block content %}
<h3>Request Email Confirmation </h3>
<p>Please confirm this email address with GetTogether.Community by clicking on the link below:</p>
<p><a href="{{confirmation_url}}" class="btn btn-success">Confirm email</a></p>
<hr/>
<p>This is an automated email sent by <a href="https://gettogether.community">https://gettogether.community</a></p>
<p>Learn more at <a href="https://github.com/GetTogetherComm/GetTogether/">https://github.com/GetTogetherComm/GetTogether/</a></p>
{% endblock %}

View file

@ -0,0 +1,9 @@
== Request Email Confirmation ==
Please confirm this email address with GetTogether.Community by clicking on the link below:
Confirm email: {{confirmation_url}}
--
This is an automated email sent by https://gettogether.community
Learn more at https://github.com/GetTogetherComm/GetTogether/

View file

@ -0,0 +1,16 @@
{% extends "get_together/base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12 align-left">
<h2>Unable to confirm email</h2>
<p>The confirmation link is invalid. Either it does not below to {{request.user.email}} or the confirmation key has expired.</p>
<p><a href={% url 'send-confirm-email' %}" class="btn btn-primary">Resend confirmation email</a></p>
</div>
</div>
</div>
{% endblock %}

View file

@ -0,0 +1,24 @@
{% extends "get_together/base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12 align-left">
<h2>Manage email notifications</h2>
<p>GetTogether can send notifications to <strong>{{request.user.email}}</strong> about your upcoming events or new events for teams you are a member of.</p>
<p>Please confirm whether or not you would like to receive these notifcation emails.</p>
<p>
<form action={% url 'confirm-notifications' %} method="POST">
{% csrf_token %}
{{notifications_form}}
<br/>
<button type="submit" class="btn btn-primary">Save</button>
</form>
</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -1,13 +1,30 @@
{% extends "get_together/base.html" %}
{% block content %}
<h2>Profile: {{request.user}}</h2>
<form action="{% url "edit-profile" %}" method="post">
{% csrf_token %}
{% include "events/profile_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Save</button>
</form>
<div class="container">
<div class="row">
<div class="col">
<h2>Profile: {{request.user}}</h2>
<form action="{% url "edit-profile" %}" method="post">
{% csrf_token %}
{% include "events/profile_form.html" %}
<br />
<button type="submit" class="btn btn-primary">Save</button>
</form>
</div>
</div>
{% if request.user.email and not request.user.account.is_email_confirmed %}
<div class="row">
<div class="col">
<hr/>
<div class="alerts"><div class="alert alert-danger">Your email address has not been confirmed.</div></div>
<p>You will not be able to receive email notifications from GetTogether until you confirm that this address belongs to you.</p>
<p><a href="{% url 'send-confirm-email' %}" class="btn btn-success">Confirm your email</a></p>
</div>
</div>
{% endif %}
</div>
{% endblock %}

View file

@ -0,0 +1,16 @@
{% extends "get_together/base.html" %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-12 align-left">
<h2>Confirmation email sent</h2>
<p>An email has been sent to <strong>{{request.user.email}}</strong>. Click the link it contains to confirm that you have access to this email address.</p>
<p>This confirmation request will expire on {{confirmation.expires}}</p>
</div>
</div>
</div>
{% endblock %}

View file

@ -36,6 +36,9 @@ urlpatterns = [
path('api/find_city/', event_views.find_city),
path('profile/+edit', views.edit_profile, name='edit-profile'),
path('profile/+send_confirmation_email', views.user_send_confirmation_email, name='send-confirm-email'),
path('profile/+confirm_email/<str:confirmation_key>', views.user_confirm_email, name='confirm-email'),
path('profile/+confirm_notifications', views.user_confirm_notifications, name='confirm-notifications'),
path('profile/<int:user_id>/', views.show_profile, name='show-profile'),
path('events/', views.events_list, name='events'),

View file

@ -23,6 +23,7 @@ from .teams import *
from .events import *
from .places import *
from .user import *
from .new_user import *
KM_PER_DEGREE_LAT = 110.574
KM_PER_DEGREE_LNG = 111.320 # At the equator

View file

@ -1,14 +1,17 @@
from django.utils.translation import ugettext_lazy as _
from django.contrib.sites.models import Site
from django.contrib import messages
from django.contrib.auth import logout as logout_user
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from django.http import HttpResponse, JsonResponse
from django.urls import reverse
from django.core.mail import send_mail
from django.template.loader import get_template, render_to_string
from django.conf import settings
from events.models.profiles import Team, UserProfile, Member
from events.forms import TeamForm, NewTeamForm, TeamEventForm, NewTeamEventForm, NewPlaceForm
from events.models.events import Event, Place, Attendee
from events.forms import SendNotificationsForm
import datetime
import simplejson
@ -22,3 +25,50 @@ def new_user_find_teams(request):
def new_user_find_events(request):
pass
# These views are for confirming a user's email address before sending them mail
@login_required
def user_send_confirmation_email(request):
confirmation_request = request.user.account.new_confirmation_request()
site = Site.objects.get(id=1)
confirmation_url = "https://%s%s" % (site.domain, reverse('confirm-email', kwargs={'confirmation_key':confirmation_request.key}))
context = {
'confirmation': confirmation_request,
'confirmation_url': confirmation_url,
}
email_subject = '[GetTogether] Confirm email address'
email_body_text = render_to_string('get_together/emails/confirm_email.txt', context, request)
email_body_html = render_to_string('get_together/emails/confirm_email.html', context, request)
email_recipients = [request.user.email]
email_from = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@gettogether.community')
send_mail(
subject=email_subject,
message=email_body_text,
from_email=email_from,
recipient_list=email_recipients,
html_message=email_body_html
)
return render(request, 'get_together/users/sent_email_confirmation.html', context)
@login_required
def user_confirm_email(request, confirmation_key):
if request.user.account.confirm_email(confirmation_key):
messages.add_message(request, messages.SUCCESS, message=_('Your email address has been confirmed.'))
return redirect('confirm-notifications')
else:
return render(request, 'get_together/users/bad_email_confirmation.html')
@login_required
def user_confirm_notifications(request):
if request.method == 'GET':
form = SendNotificationsForm(instance=request.user.profile)
context = {
'notifications_form': form
}
return render(request, 'get_together/users/confirm_notifications.html', context)
elif request.method == 'POST':
form = SendNotificationsForm(request.POST, instance=request.user.profile)
if form.is_valid():
form.save()
return redirect('home')

View file

@ -55,6 +55,7 @@ def edit_profile(request):
user = request.user
profile = request.user.profile
account = request.user.account
if request.method == 'GET':
user_form = UserForm(instance=user)
@ -66,11 +67,20 @@ def edit_profile(request):
}
return render(request, 'get_together/users/edit_profile.html', context)
elif request.method == 'POST':
old_email = request.user.email
user_form = UserForm(request.POST, instance=user)
profile_form = UserProfileForm(request.POST, instance=profile)
if user_form.is_valid() and profile_form.is_valid():
user = user_form.save()
profile = profile_form.save()
if user.email != old_email:
if user.email is None or user.email == "":
messages.add_message(request, messages.ERROR, message=_('Your email address has been removed.'))
account.is_email_confirmed = False
account.save()
else:
messages.add_message(request, messages.WARNING, message=_('Your email address has changed, please confirm your new address.'))
return redirect('send-confirm-email')
return redirect('home')
else:
context = {