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:
parent
30e2aed4af
commit
ba69749cc5
16 changed files with 294 additions and 13 deletions
31
accounts/migrations/0002_auto_20180226_1532.py
Normal file
31
accounts/migrations/0002_auto_20180226_1532.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -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)
|
||||
|
|
|
@ -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'))
|
||||
|
|
18
events/migrations/0010_userprofile_send_notifications.py
Normal file
18
events/migrations/0010_userprofile_send_notifications.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -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',)
|
||||
|
||||
|
|
29
get_together/templates/get_together/emails/base.html
Normal file
29
get_together/templates/get_together/emails/base.html
Normal 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>
|
|
@ -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 %}
|
|
@ -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/
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
||||
|
||||
|
|
|
@ -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 %}
|
||||
|
|
@ -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'),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in a new issue