From 18095ed465f262ac37f8cbef2425ec50ffd48bec Mon Sep 17 00:00:00 2001 From: Andrew Buss Date: Thu, 30 Apr 2015 18:58:59 -0700 Subject: [PATCH] Integrated django-simple-email-confirmation --- .gitignore | 3 +- flashcards/api.py | 76 +++++++++------- flashcards/migrations/0001_initial.py | 78 ----------------- flashcards/migrations/0002_auto_20150429_0248.py | 26 ------ flashcards/migrations/0003_auto_20150429_0344.py | 105 ----------------------- flashcards/migrations/0004_auto_20150429_0827.py | 42 --------- flashcards/migrations/0005_auto_20150430_0557.py | 28 ------ flashcards/migrations/0006_auto_20150430_0643.py | 19 ---- flashcards/migrations/0007_auto_20150430_0657.py | 23 ----- flashcards/migrations/__init__.py | 0 flashcards/models.py | 46 +++++++++- flashcards/serializers.py | 13 ++- flashcards/views.py | 4 +- flashy/settings.py | 19 ++-- requirements.txt | 2 + 15 files changed, 117 insertions(+), 367 deletions(-) delete mode 100644 flashcards/migrations/0001_initial.py delete mode 100644 flashcards/migrations/0002_auto_20150429_0248.py delete mode 100644 flashcards/migrations/0003_auto_20150429_0344.py delete mode 100644 flashcards/migrations/0004_auto_20150429_0827.py delete mode 100644 flashcards/migrations/0005_auto_20150430_0557.py delete mode 100644 flashcards/migrations/0006_auto_20150430_0643.py delete mode 100644 flashcards/migrations/0007_auto_20150430_0657.py delete mode 100644 flashcards/migrations/__init__.py diff --git a/.gitignore b/.gitignore index ff43edd..e4bf98a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ static* *.pyc .idea* .*.swp -*.sqlite3 \ No newline at end of file +*.sqlite3 +secrets \ No newline at end of file diff --git a/flashcards/api.py b/flashcards/api.py index abbe4fa..51b9543 100644 --- a/flashcards/api.py +++ b/flashcards/api.py @@ -1,41 +1,59 @@ -from django.http import Http404 +from django.core.mail import send_mail from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from rest_framework.exceptions import ValidationError from flashcards.serializers import * -from django.http import HttpResponse -from rest_framework.renderers import JSONRenderer - -class JSONResponse(HttpResponse): - """ - An HttpResponse that renders its content into JSON. - """ - def __init__(self, data, **kwargs): - content = JSONRenderer().render(data) - kwargs['content_type'] = 'application/json' - super(JSONResponse, self).__init__(content, **kwargs) class UserDetail(APIView): - def patch(self, request,format=None): - """ - Updates a user's password after they enter a valid old password. - TODO: email verification - """ - currentUser = request.user - if 'old_password' not in request.data: - raise ValidationError('Old password is required') - if 'new_password' not in request.data: - raise ValidationError('New password is required') - if not request.data['new_password']: - raise ValidationError('Password cannot be blank') - if not currentUser.check_password(request.data['old_password']): - raise ValidationError('Invalid old password') - currentUser.set_password(request.data['new_password']) - currentUser.save() + def patch(self, request, format=None): + """ + Updates a user's password after they enter a valid old password. + TODO: email verification + """ + + if 'old_password' not in request.data: + raise ValidationError('Old password is required') + if 'new_password' not in request.data: + raise ValidationError('New password is required') + if not request.data['new_password']: + raise ValidationError('Password cannot be blank') + + currentuser = request.user + + if not currentuser.check_password(request.data['old_password']): + raise ValidationError('Invalid old password') + + currentuser.set_password(request.data['new_password']) + currentuser.save() + return Response(status=status.HTTP_204_NO_CONTENT) - def get(self, request,format=None): + def get(self, request, format=None): serializer = UserSerializer(request.user) return Response(serializer.data) + + def post(self, request, format=None): + if 'email' not in request.data: + raise ValidationError('Email is required') + if 'password' not in request.data: + raise ValidationError('Password is required') + + email = request.data['email'] + user = User.objects.create_user(email) + + body = ''' + Visit the following link to confirm your email address: + http://flashy.cards/app/verify_email/%s + + If you did not register for Flashy, no action is required. + ''' + + send_mail("Please verify your Flashy account", + body % user.confirmation_key, + "noreply@flashy.cards", + [user.email]) + + return Response(User) + diff --git a/flashcards/migrations/0001_initial.py b/flashcards/migrations/0001_initial.py deleted file mode 100644 index 76da6c6..0000000 --- a/flashcards/migrations/0001_initial.py +++ /dev/null @@ -1,78 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations -from django.conf import settings - - -class Migration(migrations.Migration): - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Class', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('department', models.CharField(max_length=50)), - ('course_num', models.IntegerField()), - ('name', models.CharField(max_length=50)), - ('professor', models.CharField(max_length=50)), - ('quarter', models.CharField(max_length=4)), - ('members', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='Flashcard', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('text', models.CharField(max_length=255)), - ('pushed', models.DateTimeField()), - ('material_date', models.DateTimeField()), - ('hidden', models.CharField(max_length=255, null=True, blank=True)), - ('associated_class', models.ForeignKey(to='flashcards.Class')), - ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='FlashcardMask', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('ranges', models.CharField(max_length=255)), - ], - ), - migrations.CreateModel( - name='UserFlashcard', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('pulled', models.DateTimeField(null=True)), - ('unpulled', models.DateTimeField(null=True)), - ('flashcard', models.ForeignKey(to='flashcards.Flashcard')), - ('mask', models.ForeignKey(to='flashcards.FlashcardMask')), - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='UserFlashCardReview', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('when', models.DateTimeField()), - ('blanked_word', models.CharField(max_length=8)), - ('response', models.CharField(max_length=255, null=True, blank=True)), - ('correct', models.NullBooleanField()), - ('user_flashcard', models.ForeignKey(to='flashcards.UserFlashcard')), - ], - ), - migrations.AddField( - model_name='flashcard', - name='mask', - field=models.ForeignKey(to='flashcards.FlashcardMask', null=True), - ), - migrations.AddField( - model_name='flashcard', - name='previous', - field=models.ForeignKey(to='flashcards.Flashcard', null=True), - ), - ] diff --git a/flashcards/migrations/0002_auto_20150429_0248.py b/flashcards/migrations/0002_auto_20150429_0248.py deleted file mode 100644 index 642a514..0000000 --- a/flashcards/migrations/0002_auto_20150429_0248.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0001_initial'), - ] - - operations = [ - migrations.AlterModelOptions( - name='userflashcard', - options={'ordering': ['-pulled']}, - ), - migrations.AlterUniqueTogether( - name='userflashcard', - unique_together=set([('user', 'flashcard')]), - ), - migrations.AlterIndexTogether( - name='userflashcard', - index_together=set([('user', 'flashcard')]), - ), - ] diff --git a/flashcards/migrations/0003_auto_20150429_0344.py b/flashcards/migrations/0003_auto_20150429_0344.py deleted file mode 100644 index 5e126ef..0000000 --- a/flashcards/migrations/0003_auto_20150429_0344.py +++ /dev/null @@ -1,105 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0002_auto_20150429_0248'), - ] - - operations = [ - migrations.AlterModelOptions( - name='class', - options={'ordering': ['-quarter']}, - ), - migrations.AlterModelOptions( - name='flashcard', - options={'ordering': ['-pushed']}, - ), - migrations.RenameField( - model_name='class', - old_name='professor', - new_name='instructor', - ), - migrations.RemoveField( - model_name='flashcard', - name='hidden', - ), - migrations.AddField( - model_name='flashcard', - name='hide_reason', - field=models.CharField(help_text=b'Reason for hiding this card', max_length=255, blank=True), - ), - migrations.AddField( - model_name='flashcard', - name='is_hidden', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='flashcard', - name='associated_class', - field=models.ForeignKey(help_text=b'The class with which the card is associated', to='flashcards.Class'), - ), - migrations.AlterField( - model_name='flashcard', - name='mask', - field=models.ForeignKey(blank=True, to='flashcards.FlashcardMask', help_text=b'The default mask for this card', null=True), - ), - migrations.AlterField( - model_name='flashcard', - name='material_date', - field=models.DateTimeField(help_text=b'The date with which the card is associated'), - ), - migrations.AlterField( - model_name='flashcard', - name='previous', - field=models.ForeignKey(blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), - ), - migrations.AlterField( - model_name='flashcard', - name='pushed', - field=models.DateTimeField(help_text=b'When the card was first pushed', auto_now_add=True), - ), - migrations.AlterField( - model_name='flashcard', - name='text', - field=models.CharField(help_text=b'The text on the card', max_length=255), - ), - migrations.AlterField( - model_name='userflashcard', - name='mask', - field=models.ForeignKey(help_text=b"A mask which overrides the card's mask", to='flashcards.FlashcardMask'), - ), - migrations.AlterField( - model_name='userflashcard', - name='pulled', - field=models.DateTimeField(help_text=b'When the user pulled the card', null=True, blank=True), - ), - migrations.AlterField( - model_name='userflashcard', - name='unpulled', - field=models.DateTimeField(help_text=b'When the user unpulled this card', null=True, blank=True), - ), - migrations.AlterField( - model_name='userflashcardreview', - name='blanked_word', - field=models.CharField(help_text=b'The character range which was blanked', max_length=8, blank=True), - ), - migrations.AlterField( - model_name='userflashcardreview', - name='correct', - field=models.NullBooleanField(help_text=b"The user's self-evaluation of their response"), - ), - migrations.AlterField( - model_name='userflashcardreview', - name='response', - field=models.CharField(help_text=b"The user's response", max_length=255, null=True, blank=True), - ), - migrations.AlterUniqueTogether( - name='class', - unique_together=set([('department', 'course_num', 'quarter', 'instructor')]), - ), - ] diff --git a/flashcards/migrations/0004_auto_20150429_0827.py b/flashcards/migrations/0004_auto_20150429_0827.py deleted file mode 100644 index 3926f5b..0000000 --- a/flashcards/migrations/0004_auto_20150429_0827.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0003_auto_20150429_0344'), - ] - - operations = [ - migrations.CreateModel( - name='LecturePeriod', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('week_day', models.IntegerField(help_text=b'0-indexed day of week, starting at Monday')), - ('start_time', models.TimeField()), - ('end_time', models.TimeField()), - ], - ), - migrations.RenameModel( - old_name='Class', - new_name='Section', - ), - migrations.RemoveField( - model_name='flashcard', - name='associated_class', - ), - migrations.AddField( - model_name='flashcard', - name='section', - field=models.ForeignKey(default=None, to='flashcards.Section', help_text=b'The section with which the card is associated'), - preserve_default=False, - ), - migrations.AddField( - model_name='lectureperiod', - name='section', - field=models.ForeignKey(to='flashcards.Section'), - ), - ] diff --git a/flashcards/migrations/0005_auto_20150430_0557.py b/flashcards/migrations/0005_auto_20150430_0557.py deleted file mode 100644 index 7e56ca5..0000000 --- a/flashcards/migrations/0005_auto_20150430_0557.py +++ /dev/null @@ -1,28 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0004_auto_20150429_0827'), - ] - - operations = [ - migrations.RenameField( - model_name='section', - old_name='name', - new_name='course_title', - ), - migrations.RenameField( - model_name='section', - old_name='instructor', - new_name='professor', - ), - migrations.AlterUniqueTogether( - name='section', - unique_together=set([('department', 'course_num', 'quarter', 'professor')]), - ), - ] diff --git a/flashcards/migrations/0006_auto_20150430_0643.py b/flashcards/migrations/0006_auto_20150430_0643.py deleted file mode 100644 index fd9a753..0000000 --- a/flashcards/migrations/0006_auto_20150430_0643.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0005_auto_20150430_0557'), - ] - - operations = [ - migrations.AlterField( - model_name='section', - name='course_num', - field=models.CharField(max_length=6), - ), - ] diff --git a/flashcards/migrations/0007_auto_20150430_0657.py b/flashcards/migrations/0007_auto_20150430_0657.py deleted file mode 100644 index bad93e8..0000000 --- a/flashcards/migrations/0007_auto_20150430_0657.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import models, migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('flashcards', '0006_auto_20150430_0643'), - ] - - operations = [ - migrations.RenameField( - model_name='section', - old_name='professor', - new_name='instructor', - ), - migrations.AlterUniqueTogether( - name='section', - unique_together=set([('department', 'course_num', 'quarter', 'instructor')]), - ), - ] diff --git a/flashcards/migrations/__init__.py b/flashcards/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/flashcards/models.py b/flashcards/models.py index f2a87dc..610db2e 100644 --- a/flashcards/models.py +++ b/flashcards/models.py @@ -1,5 +1,47 @@ -from django.contrib.auth.models import User +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager +from django.contrib.auth.tests.custom_user import CustomUser from django.db.models import * +from simple_email_confirmation import SimpleEmailConfirmationUserMixin + + +class UserManager(BaseUserManager): + def create_user(self, email, password=None): + """ + Creates and saves a User with the given email, date of + birth and password. + """ + if not email: + raise ValueError('Users must have an email address') + + user = self.model(email=self.normalize_email(email)) + + user.set_password(password) + user.save(using=self._db) + return user + + def create_superuser(self, email, password): + """ + Creates and saves a superuser with the given email and password. + """ + user = self.create_user(email, password=password) + user.is_admin = True + user.save(using=self._db) + return user + + +class User(AbstractBaseUser, SimpleEmailConfirmationUserMixin): + USERNAME_FIELD = 'email' + REQUIRED_FIELDS = [] + + objects = UserManager() + + email = EmailField( + verbose_name='email address', + max_length=255, + unique=True, + ) + date_joined = DateTimeField(auto_now_add=True) + sections = ManyToManyField('Section') class UserFlashcard(Model): @@ -104,6 +146,7 @@ class UserFlashcardReview(Model): if self.response: return "answered" return "viewed" + class Section(Model): """ A UCSD course taught by an instructor during a quarter. @@ -116,7 +159,6 @@ class Section(Model): course_title = CharField(max_length=50) instructor = CharField(max_length=50) quarter = CharField(max_length=4) - members = ManyToManyField(User) class Meta: unique_together = (('department', 'course_num', 'quarter', 'instructor'),) diff --git a/flashcards/serializers.py b/flashcards/serializers.py index 6222e66..da1e2cd 100644 --- a/flashcards/serializers.py +++ b/flashcards/serializers.py @@ -1,10 +1,12 @@ -from flashcards.models import Section, LecturePeriod +from flashcards.models import Section, LecturePeriod, User +from rest_framework.fields import EmailField from rest_framework.relations import HyperlinkedRelatedField from rest_framework.serializers import HyperlinkedModelSerializer -from django.contrib.auth.models import User + class SectionSerializer(HyperlinkedModelSerializer): lectureperiod_set = HyperlinkedRelatedField(many=True, view_name='lectureperiod-detail', read_only=True) + class Meta: model = Section exclude = ('members',) @@ -14,9 +16,12 @@ class LecturePeriodSerializer(HyperlinkedModelSerializer): class Meta: model = LecturePeriod + class UserSerializer(HyperlinkedModelSerializer): """ """ + email = EmailField(required=False) + class Meta: - model = User - fields = ("email", "is_active", "last_login", "date_joined") + model = User + fields = ("email", "is_active", "last_login", "date_joined") diff --git a/flashcards/views.py b/flashcards/views.py index 30791cd..c8d1ab7 100644 --- a/flashcards/views.py +++ b/flashcards/views.py @@ -3,19 +3,21 @@ from flashcards.serializers import SectionSerializer, LecturePeriodSerializer from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.viewsets import ModelViewSet from rest_framework.pagination import PageNumberPagination -from django.core.paginator import Paginator + class StandardResultsSetPagination(PageNumberPagination): page_size = 40 page_size_query_param = 'page_size' max_page_size = 1000 + class SectionViewSet(ModelViewSet): queryset = Section.objects.all() serializer_class = SectionSerializer permission_classes = (IsAuthenticatedOrReadOnly,) pagination_class = StandardResultsSetPagination + class LecturePeriodViewSet(ModelViewSet): queryset = LecturePeriod.objects.all() serializer_class = LecturePeriodSerializer diff --git a/flashy/settings.py b/flashy/settings.py index 6a3d0e1..3feeb36 100644 --- a/flashy/settings.py +++ b/flashy/settings.py @@ -15,22 +15,17 @@ import os BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = '8)#-j&8dghe-y&v4a%r6u#y@r86&6nfv%k$(a((r-zh$m3ct!9' - # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] - +AUTH_USER_MODEL = 'flashcards.User' # Application definition INSTALLED_APPS = ( + 'simple_email_confirmation', + 'flashcards', 'django.contrib.admin', 'django.contrib.admindocs', 'django.contrib.auth', @@ -38,8 +33,10 @@ INSTALLED_APPS = ( 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'django_ses', 'rest_framework', - 'flashcards', + + ) REST_FRAMEWORK = { @@ -109,3 +106,7 @@ USE_TZ = True STATIC_URL = '/static/' STATIC_ROOT = 'static' + +EMAIL_BACKEND = 'django_ses.SESBackend' + +SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY') \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 44261de..cad06ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,5 @@ six==1.9.0 djangorestframework docutils gunicorn +django-simple-email-confirmation +django-ses -- 1.9.1