Commit 18095ed465f262ac37f8cbef2425ec50ffd48bec

Authored by Andrew Buss
1 parent 2dc11d15d8
Exists in master

Integrated django-simple-email-confirmation

Showing 14 changed files with 117 additions and 367 deletions Side-by-side Diff

... ... @@ -5,4 +5,5 @@
5 5 .idea*
6 6 .*.swp
7 7 *.sqlite3
  8 +secrets
flashcards/api.py View file @ 18095ed
1   -from django.http import Http404
  1 +from django.core.mail import send_mail
2 2 from rest_framework.views import APIView
3 3 from rest_framework.response import Response
4 4 from rest_framework import status
5 5 from rest_framework.exceptions import ValidationError
6 6 from flashcards.serializers import *
7   -from django.http import HttpResponse
8   -from rest_framework.renderers import JSONRenderer
9 7  
10   -class JSONResponse(HttpResponse):
11   - """
12   - An HttpResponse that renders its content into JSON.
13   - """
14   - def __init__(self, data, **kwargs):
15   - content = JSONRenderer().render(data)
16   - kwargs['content_type'] = 'application/json'
17   - super(JSONResponse, self).__init__(content, **kwargs)
18 8  
19   -
20 9 class UserDetail(APIView):
21   - def patch(self, request,format=None):
22   - """
23   - Updates a user's password after they enter a valid old password.
24   - TODO: email verification
25   - """
26   - currentUser = request.user
27   - if 'old_password' not in request.data:
28   - raise ValidationError('Old password is required')
29   - if 'new_password' not in request.data:
30   - raise ValidationError('New password is required')
31   - if not request.data['new_password']:
32   - raise ValidationError('Password cannot be blank')
33   - if not currentUser.check_password(request.data['old_password']):
34   - raise ValidationError('Invalid old password')
35   - currentUser.set_password(request.data['new_password'])
36   - currentUser.save()
  10 + def patch(self, request, format=None):
  11 + """
  12 + Updates a user's password after they enter a valid old password.
  13 + TODO: email verification
  14 + """
  15 +
  16 + if 'old_password' not in request.data:
  17 + raise ValidationError('Old password is required')
  18 + if 'new_password' not in request.data:
  19 + raise ValidationError('New password is required')
  20 + if not request.data['new_password']:
  21 + raise ValidationError('Password cannot be blank')
  22 +
  23 + currentuser = request.user
  24 +
  25 + if not currentuser.check_password(request.data['old_password']):
  26 + raise ValidationError('Invalid old password')
  27 +
  28 + currentuser.set_password(request.data['new_password'])
  29 + currentuser.save()
  30 +
37 31 return Response(status=status.HTTP_204_NO_CONTENT)
38 32  
39   - def get(self, request,format=None):
  33 + def get(self, request, format=None):
40 34 serializer = UserSerializer(request.user)
41 35 return Response(serializer.data)
  36 +
  37 + def post(self, request, format=None):
  38 + if 'email' not in request.data:
  39 + raise ValidationError('Email is required')
  40 + if 'password' not in request.data:
  41 + raise ValidationError('Password is required')
  42 +
  43 + email = request.data['email']
  44 + user = User.objects.create_user(email)
  45 +
  46 + body = '''
  47 + Visit the following link to confirm your email address:
  48 + http://flashy.cards/app/verify_email/%s
  49 +
  50 + If you did not register for Flashy, no action is required.
  51 + '''
  52 +
  53 + send_mail("Please verify your Flashy account",
  54 + body % user.confirmation_key,
  55 + "noreply@flashy.cards",
  56 + [user.email])
  57 +
  58 + return Response(User)
flashcards/migrations/0001_initial.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -from django.conf import settings
6   -
7   -
8   -class Migration(migrations.Migration):
9   -
10   - dependencies = [
11   - migrations.swappable_dependency(settings.AUTH_USER_MODEL),
12   - ]
13   -
14   - operations = [
15   - migrations.CreateModel(
16   - name='Class',
17   - fields=[
18   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
19   - ('department', models.CharField(max_length=50)),
20   - ('course_num', models.IntegerField()),
21   - ('name', models.CharField(max_length=50)),
22   - ('professor', models.CharField(max_length=50)),
23   - ('quarter', models.CharField(max_length=4)),
24   - ('members', models.ManyToManyField(to=settings.AUTH_USER_MODEL)),
25   - ],
26   - ),
27   - migrations.CreateModel(
28   - name='Flashcard',
29   - fields=[
30   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
31   - ('text', models.CharField(max_length=255)),
32   - ('pushed', models.DateTimeField()),
33   - ('material_date', models.DateTimeField()),
34   - ('hidden', models.CharField(max_length=255, null=True, blank=True)),
35   - ('associated_class', models.ForeignKey(to='flashcards.Class')),
36   - ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
37   - ],
38   - ),
39   - migrations.CreateModel(
40   - name='FlashcardMask',
41   - fields=[
42   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
43   - ('ranges', models.CharField(max_length=255)),
44   - ],
45   - ),
46   - migrations.CreateModel(
47   - name='UserFlashcard',
48   - fields=[
49   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
50   - ('pulled', models.DateTimeField(null=True)),
51   - ('unpulled', models.DateTimeField(null=True)),
52   - ('flashcard', models.ForeignKey(to='flashcards.Flashcard')),
53   - ('mask', models.ForeignKey(to='flashcards.FlashcardMask')),
54   - ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
55   - ],
56   - ),
57   - migrations.CreateModel(
58   - name='UserFlashCardReview',
59   - fields=[
60   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
61   - ('when', models.DateTimeField()),
62   - ('blanked_word', models.CharField(max_length=8)),
63   - ('response', models.CharField(max_length=255, null=True, blank=True)),
64   - ('correct', models.NullBooleanField()),
65   - ('user_flashcard', models.ForeignKey(to='flashcards.UserFlashcard')),
66   - ],
67   - ),
68   - migrations.AddField(
69   - model_name='flashcard',
70   - name='mask',
71   - field=models.ForeignKey(to='flashcards.FlashcardMask', null=True),
72   - ),
73   - migrations.AddField(
74   - model_name='flashcard',
75   - name='previous',
76   - field=models.ForeignKey(to='flashcards.Flashcard', null=True),
77   - ),
78   - ]
flashcards/migrations/0002_auto_20150429_0248.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0001_initial'),
11   - ]
12   -
13   - operations = [
14   - migrations.AlterModelOptions(
15   - name='userflashcard',
16   - options={'ordering': ['-pulled']},
17   - ),
18   - migrations.AlterUniqueTogether(
19   - name='userflashcard',
20   - unique_together=set([('user', 'flashcard')]),
21   - ),
22   - migrations.AlterIndexTogether(
23   - name='userflashcard',
24   - index_together=set([('user', 'flashcard')]),
25   - ),
26   - ]
flashcards/migrations/0003_auto_20150429_0344.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0002_auto_20150429_0248'),
11   - ]
12   -
13   - operations = [
14   - migrations.AlterModelOptions(
15   - name='class',
16   - options={'ordering': ['-quarter']},
17   - ),
18   - migrations.AlterModelOptions(
19   - name='flashcard',
20   - options={'ordering': ['-pushed']},
21   - ),
22   - migrations.RenameField(
23   - model_name='class',
24   - old_name='professor',
25   - new_name='instructor',
26   - ),
27   - migrations.RemoveField(
28   - model_name='flashcard',
29   - name='hidden',
30   - ),
31   - migrations.AddField(
32   - model_name='flashcard',
33   - name='hide_reason',
34   - field=models.CharField(help_text=b'Reason for hiding this card', max_length=255, blank=True),
35   - ),
36   - migrations.AddField(
37   - model_name='flashcard',
38   - name='is_hidden',
39   - field=models.BooleanField(default=False),
40   - ),
41   - migrations.AlterField(
42   - model_name='flashcard',
43   - name='associated_class',
44   - field=models.ForeignKey(help_text=b'The class with which the card is associated', to='flashcards.Class'),
45   - ),
46   - migrations.AlterField(
47   - model_name='flashcard',
48   - name='mask',
49   - field=models.ForeignKey(blank=True, to='flashcards.FlashcardMask', help_text=b'The default mask for this card', null=True),
50   - ),
51   - migrations.AlterField(
52   - model_name='flashcard',
53   - name='material_date',
54   - field=models.DateTimeField(help_text=b'The date with which the card is associated'),
55   - ),
56   - migrations.AlterField(
57   - model_name='flashcard',
58   - name='previous',
59   - field=models.ForeignKey(blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True),
60   - ),
61   - migrations.AlterField(
62   - model_name='flashcard',
63   - name='pushed',
64   - field=models.DateTimeField(help_text=b'When the card was first pushed', auto_now_add=True),
65   - ),
66   - migrations.AlterField(
67   - model_name='flashcard',
68   - name='text',
69   - field=models.CharField(help_text=b'The text on the card', max_length=255),
70   - ),
71   - migrations.AlterField(
72   - model_name='userflashcard',
73   - name='mask',
74   - field=models.ForeignKey(help_text=b"A mask which overrides the card's mask", to='flashcards.FlashcardMask'),
75   - ),
76   - migrations.AlterField(
77   - model_name='userflashcard',
78   - name='pulled',
79   - field=models.DateTimeField(help_text=b'When the user pulled the card', null=True, blank=True),
80   - ),
81   - migrations.AlterField(
82   - model_name='userflashcard',
83   - name='unpulled',
84   - field=models.DateTimeField(help_text=b'When the user unpulled this card', null=True, blank=True),
85   - ),
86   - migrations.AlterField(
87   - model_name='userflashcardreview',
88   - name='blanked_word',
89   - field=models.CharField(help_text=b'The character range which was blanked', max_length=8, blank=True),
90   - ),
91   - migrations.AlterField(
92   - model_name='userflashcardreview',
93   - name='correct',
94   - field=models.NullBooleanField(help_text=b"The user's self-evaluation of their response"),
95   - ),
96   - migrations.AlterField(
97   - model_name='userflashcardreview',
98   - name='response',
99   - field=models.CharField(help_text=b"The user's response", max_length=255, null=True, blank=True),
100   - ),
101   - migrations.AlterUniqueTogether(
102   - name='class',
103   - unique_together=set([('department', 'course_num', 'quarter', 'instructor')]),
104   - ),
105   - ]
flashcards/migrations/0004_auto_20150429_0827.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0003_auto_20150429_0344'),
11   - ]
12   -
13   - operations = [
14   - migrations.CreateModel(
15   - name='LecturePeriod',
16   - fields=[
17   - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
18   - ('week_day', models.IntegerField(help_text=b'0-indexed day of week, starting at Monday')),
19   - ('start_time', models.TimeField()),
20   - ('end_time', models.TimeField()),
21   - ],
22   - ),
23   - migrations.RenameModel(
24   - old_name='Class',
25   - new_name='Section',
26   - ),
27   - migrations.RemoveField(
28   - model_name='flashcard',
29   - name='associated_class',
30   - ),
31   - migrations.AddField(
32   - model_name='flashcard',
33   - name='section',
34   - field=models.ForeignKey(default=None, to='flashcards.Section', help_text=b'The section with which the card is associated'),
35   - preserve_default=False,
36   - ),
37   - migrations.AddField(
38   - model_name='lectureperiod',
39   - name='section',
40   - field=models.ForeignKey(to='flashcards.Section'),
41   - ),
42   - ]
flashcards/migrations/0005_auto_20150430_0557.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0004_auto_20150429_0827'),
11   - ]
12   -
13   - operations = [
14   - migrations.RenameField(
15   - model_name='section',
16   - old_name='name',
17   - new_name='course_title',
18   - ),
19   - migrations.RenameField(
20   - model_name='section',
21   - old_name='instructor',
22   - new_name='professor',
23   - ),
24   - migrations.AlterUniqueTogether(
25   - name='section',
26   - unique_together=set([('department', 'course_num', 'quarter', 'professor')]),
27   - ),
28   - ]
flashcards/migrations/0006_auto_20150430_0643.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0005_auto_20150430_0557'),
11   - ]
12   -
13   - operations = [
14   - migrations.AlterField(
15   - model_name='section',
16   - name='course_num',
17   - field=models.CharField(max_length=6),
18   - ),
19   - ]
flashcards/migrations/0007_auto_20150430_0657.py View file @ 18095ed
1   -# -*- coding: utf-8 -*-
2   -from __future__ import unicode_literals
3   -
4   -from django.db import models, migrations
5   -
6   -
7   -class Migration(migrations.Migration):
8   -
9   - dependencies = [
10   - ('flashcards', '0006_auto_20150430_0643'),
11   - ]
12   -
13   - operations = [
14   - migrations.RenameField(
15   - model_name='section',
16   - old_name='professor',
17   - new_name='instructor',
18   - ),
19   - migrations.AlterUniqueTogether(
20   - name='section',
21   - unique_together=set([('department', 'course_num', 'quarter', 'instructor')]),
22   - ),
23   - ]
flashcards/models.py View file @ 18095ed
1   -from django.contrib.auth.models import User
  1 +from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
  2 +from django.contrib.auth.tests.custom_user import CustomUser
2 3 from django.db.models import *
  4 +from simple_email_confirmation import SimpleEmailConfirmationUserMixin
3 5  
4 6  
  7 +class UserManager(BaseUserManager):
  8 + def create_user(self, email, password=None):
  9 + """
  10 + Creates and saves a User with the given email, date of
  11 + birth and password.
  12 + """
  13 + if not email:
  14 + raise ValueError('Users must have an email address')
  15 +
  16 + user = self.model(email=self.normalize_email(email))
  17 +
  18 + user.set_password(password)
  19 + user.save(using=self._db)
  20 + return user
  21 +
  22 + def create_superuser(self, email, password):
  23 + """
  24 + Creates and saves a superuser with the given email and password.
  25 + """
  26 + user = self.create_user(email, password=password)
  27 + user.is_admin = True
  28 + user.save(using=self._db)
  29 + return user
  30 +
  31 +
  32 +class User(AbstractBaseUser, SimpleEmailConfirmationUserMixin):
  33 + USERNAME_FIELD = 'email'
  34 + REQUIRED_FIELDS = []
  35 +
  36 + objects = UserManager()
  37 +
  38 + email = EmailField(
  39 + verbose_name='email address',
  40 + max_length=255,
  41 + unique=True,
  42 + )
  43 + date_joined = DateTimeField(auto_now_add=True)
  44 + sections = ManyToManyField('Section')
  45 +
  46 +
5 47 class UserFlashcard(Model):
6 48 """
7 49 Represents the relationship between a user and a flashcard by:
... ... @@ -104,6 +146,7 @@
104 146 if self.response: return "answered"
105 147 return "viewed"
106 148  
  149 +
107 150 class Section(Model):
108 151 """
109 152 A UCSD course taught by an instructor during a quarter.
... ... @@ -116,7 +159,6 @@
116 159 course_title = CharField(max_length=50)
117 160 instructor = CharField(max_length=50)
118 161 quarter = CharField(max_length=4)
119   - members = ManyToManyField(User)
120 162  
121 163 class Meta:
122 164 unique_together = (('department', 'course_num', 'quarter', 'instructor'),)
flashcards/serializers.py View file @ 18095ed
1   -from flashcards.models import Section, LecturePeriod
  1 +from flashcards.models import Section, LecturePeriod, User
  2 +from rest_framework.fields import EmailField
2 3 from rest_framework.relations import HyperlinkedRelatedField
3 4 from rest_framework.serializers import HyperlinkedModelSerializer
4   -from django.contrib.auth.models import User
5 5  
  6 +
6 7 class SectionSerializer(HyperlinkedModelSerializer):
7 8 lectureperiod_set = HyperlinkedRelatedField(many=True, view_name='lectureperiod-detail', read_only=True)
  9 +
8 10 class Meta:
9 11 model = Section
10 12 exclude = ('members',)
11 13  
12 14  
... ... @@ -14,10 +16,13 @@
14 16 class Meta:
15 17 model = LecturePeriod
16 18  
  19 +
17 20 class UserSerializer(HyperlinkedModelSerializer):
18 21 """
19 22 """
  23 + email = EmailField(required=False)
  24 +
20 25 class Meta:
21   - model = User
22   - fields = ("email", "is_active", "last_login", "date_joined")
  26 + model = User
  27 + fields = ("email", "is_active", "last_login", "date_joined")
flashcards/views.py View file @ 18095ed
... ... @@ -3,18 +3,20 @@
3 3 from rest_framework.permissions import IsAuthenticatedOrReadOnly
4 4 from rest_framework.viewsets import ModelViewSet
5 5 from rest_framework.pagination import PageNumberPagination
6   -from django.core.paginator import Paginator
7 6  
  7 +
8 8 class StandardResultsSetPagination(PageNumberPagination):
9 9 page_size = 40
10 10 page_size_query_param = 'page_size'
11 11 max_page_size = 1000
12 12  
  13 +
13 14 class SectionViewSet(ModelViewSet):
14 15 queryset = Section.objects.all()
15 16 serializer_class = SectionSerializer
16 17 permission_classes = (IsAuthenticatedOrReadOnly,)
17 18 pagination_class = StandardResultsSetPagination
  19 +
18 20  
19 21 class LecturePeriodViewSet(ModelViewSet):
20 22 queryset = LecturePeriod.objects.all()
flashy/settings.py View file @ 18095ed
... ... @@ -15,22 +15,17 @@
15 15  
16 16 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
17 17  
18   -
19   -# Quick-start development settings - unsuitable for production
20   -# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
21   -
22   -# SECURITY WARNING: keep the secret key used in production secret!
23   -SECRET_KEY = '8)#-j&8dghe-y&v4a%r6u#y@r86&6nfv%k$(a((r-zh$m3ct!9'
24   -
25 18 # SECURITY WARNING: don't run with debug turned on in production!
26 19 DEBUG = True
27 20  
28 21 ALLOWED_HOSTS = []
29 22  
30   -
  23 +AUTH_USER_MODEL = 'flashcards.User'
31 24 # Application definition
32 25  
33 26 INSTALLED_APPS = (
  27 + 'simple_email_confirmation',
  28 + 'flashcards',
34 29 'django.contrib.admin',
35 30 'django.contrib.admindocs',
36 31 'django.contrib.auth',
37 32  
... ... @@ -38,8 +33,10 @@
38 33 'django.contrib.sessions',
39 34 'django.contrib.messages',
40 35 'django.contrib.staticfiles',
  36 + 'django_ses',
41 37 'rest_framework',
42   - 'flashcards',
  38 +
  39 +
43 40 )
44 41  
45 42 REST_FRAMEWORK = {
... ... @@ -109,4 +106,8 @@
109 106  
110 107 STATIC_URL = '/static/'
111 108 STATIC_ROOT = 'static'
  109 +
  110 +EMAIL_BACKEND = 'django_ses.SESBackend'
  111 +
  112 +SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY')
requirements.txt View file @ 18095ed
... ... @@ -8,4 +8,6 @@
8 8 djangorestframework
9 9 docutils
10 10 gunicorn
  11 +django-simple-email-confirmation
  12 +django-ses