Commit 18095ed465f262ac37f8cbef2425ec50ffd48bec
1 parent
2dc11d15d8
Exists in
master
Integrated django-simple-email-confirmation
Showing 14 changed files with 117 additions and 367 deletions Inline Diff
- .gitignore
- flashcards/api.py
- flashcards/migrations/0001_initial.py
- flashcards/migrations/0002_auto_20150429_0248.py
- flashcards/migrations/0003_auto_20150429_0344.py
- flashcards/migrations/0004_auto_20150429_0827.py
- flashcards/migrations/0005_auto_20150430_0557.py
- flashcards/migrations/0006_auto_20150430_0643.py
- flashcards/migrations/0007_auto_20150430_0657.py
- flashcards/models.py
- flashcards/serializers.py
- flashcards/views.py
- flashy/settings.py
- requirements.txt
.gitignore
View file @
18095ed
*~ | 1 | 1 | *~ | |
venv* | 2 | 2 | venv* | |
static* | 3 | 3 | static* | |
*.pyc | 4 | 4 | *.pyc | |
.idea* | 5 | 5 | .idea* | |
.*.swp | 6 | 6 | .*.swp | |
*.sqlite3 | 7 | 7 | *.sqlite3 | |
8 | secrets |
flashcards/api.py
View file @
18095ed
from django.http import Http404 | 1 | 1 | from django.core.mail import send_mail | |
from rest_framework.views import APIView | 2 | 2 | from rest_framework.views import APIView | |
from rest_framework.response import Response | 3 | 3 | from rest_framework.response import Response | |
from rest_framework import status | 4 | 4 | from rest_framework import status | |
from rest_framework.exceptions import ValidationError | 5 | 5 | from rest_framework.exceptions import ValidationError | |
from flashcards.serializers import * | 6 | 6 | from flashcards.serializers import * | |
from django.http import HttpResponse | 7 | |||
from rest_framework.renderers import JSONRenderer | 8 | |||
9 | 7 | |||
class JSONResponse(HttpResponse): | 10 | |||
""" | 11 | |||
An HttpResponse that renders its content into JSON. | 12 | |||
""" | 13 | |||
def __init__(self, data, **kwargs): | 14 | |||
content = JSONRenderer().render(data) | 15 | |||
kwargs['content_type'] = 'application/json' | 16 | |||
super(JSONResponse, self).__init__(content, **kwargs) | 17 | |||
18 | 8 | |||
19 | ||||
class UserDetail(APIView): | 20 | 9 | class UserDetail(APIView): | |
def patch(self, request,format=None): | 21 | 10 | def patch(self, request, format=None): | |
""" | 22 | 11 | """ | |
Updates a user's password after they enter a valid old password. | 23 | 12 | Updates a user's password after they enter a valid old password. | |
TODO: email verification | 24 | 13 | TODO: email verification | |
""" | 25 | 14 | """ | |
currentUser = request.user | 26 | 15 | ||
if 'old_password' not in request.data: | 27 | 16 | if 'old_password' not in request.data: | |
raise ValidationError('Old password is required') | 28 | 17 | raise ValidationError('Old password is required') | |
if 'new_password' not in request.data: | 29 | 18 | if 'new_password' not in request.data: | |
raise ValidationError('New password is required') | 30 | 19 | raise ValidationError('New password is required') | |
if not request.data['new_password']: | 31 | 20 | if not request.data['new_password']: | |
raise ValidationError('Password cannot be blank') | 32 | 21 | raise ValidationError('Password cannot be blank') | |
if not currentUser.check_password(request.data['old_password']): | 33 | 22 | ||
raise ValidationError('Invalid old password') | 34 | 23 | currentuser = request.user | |
currentUser.set_password(request.data['new_password']) | 35 | 24 | ||
currentUser.save() | 36 | 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 | ||||
return Response(status=status.HTTP_204_NO_CONTENT) | 37 | 31 | return Response(status=status.HTTP_204_NO_CONTENT) | |
38 | 32 | |||
def get(self, request,format=None): | 39 | 33 | def get(self, request, format=None): | |
serializer = UserSerializer(request.user) | 40 | 34 | serializer = UserSerializer(request.user) | |
return Response(serializer.data) | 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) | |||
42 | 59 | |||
60 | ||||
flashcards/migrations/0001_initial.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
from django.conf import settings | 5 | |||
6 | ||||
7 | ||||
class Migration(migrations.Migration): | 8 | |||
9 | ||||
dependencies = [ | 10 | |||
migrations.swappable_dependency(settings.AUTH_USER_MODEL), | 11 | |||
] | 12 | |||
13 | ||||
operations = [ | 14 | |||
migrations.CreateModel( | 15 | |||
name='Class', | 16 | |||
fields=[ | 17 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 18 | |||
('department', models.CharField(max_length=50)), | 19 | |||
('course_num', models.IntegerField()), | 20 | |||
('name', models.CharField(max_length=50)), | 21 | |||
('professor', models.CharField(max_length=50)), | 22 | |||
('quarter', models.CharField(max_length=4)), | 23 | |||
('members', models.ManyToManyField(to=settings.AUTH_USER_MODEL)), | 24 | |||
], | 25 | |||
), | 26 | |||
migrations.CreateModel( | 27 | |||
name='Flashcard', | 28 | |||
fields=[ | 29 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 30 | |||
('text', models.CharField(max_length=255)), | 31 | |||
('pushed', models.DateTimeField()), | 32 | |||
('material_date', models.DateTimeField()), | 33 | |||
('hidden', models.CharField(max_length=255, null=True, blank=True)), | 34 | |||
('associated_class', models.ForeignKey(to='flashcards.Class')), | 35 | |||
('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)), | 36 | |||
], | 37 | |||
), | 38 | |||
migrations.CreateModel( | 39 | |||
name='FlashcardMask', | 40 | |||
fields=[ | 41 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 42 | |||
('ranges', models.CharField(max_length=255)), | 43 | |||
], | 44 | |||
), | 45 | |||
migrations.CreateModel( | 46 | |||
name='UserFlashcard', | 47 | |||
fields=[ | 48 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 49 | |||
('pulled', models.DateTimeField(null=True)), | 50 | |||
('unpulled', models.DateTimeField(null=True)), | 51 | |||
('flashcard', models.ForeignKey(to='flashcards.Flashcard')), | 52 | |||
('mask', models.ForeignKey(to='flashcards.FlashcardMask')), | 53 | |||
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)), | 54 | |||
], | 55 | |||
), | 56 | |||
migrations.CreateModel( | 57 | |||
name='UserFlashCardReview', | 58 | |||
fields=[ | 59 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 60 | |||
('when', models.DateTimeField()), | 61 | |||
('blanked_word', models.CharField(max_length=8)), | 62 | |||
('response', models.CharField(max_length=255, null=True, blank=True)), | 63 | |||
('correct', models.NullBooleanField()), | 64 | |||
('user_flashcard', models.ForeignKey(to='flashcards.UserFlashcard')), | 65 | |||
], | 66 | |||
), | 67 | |||
migrations.AddField( | 68 | |||
model_name='flashcard', | 69 | |||
name='mask', | 70 | |||
field=models.ForeignKey(to='flashcards.FlashcardMask', null=True), | 71 | |||
), | 72 | |||
migrations.AddField( | 73 |
flashcards/migrations/0002_auto_20150429_0248.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0001_initial'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.AlterModelOptions( | 14 | |||
name='userflashcard', | 15 | |||
options={'ordering': ['-pulled']}, | 16 | |||
), | 17 | |||
migrations.AlterUniqueTogether( | 18 | |||
name='userflashcard', | 19 | |||
unique_together=set([('user', 'flashcard')]), | 20 | |||
), | 21 |
flashcards/migrations/0003_auto_20150429_0344.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0002_auto_20150429_0248'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.AlterModelOptions( | 14 | |||
name='class', | 15 | |||
options={'ordering': ['-quarter']}, | 16 | |||
), | 17 | |||
migrations.AlterModelOptions( | 18 | |||
name='flashcard', | 19 | |||
options={'ordering': ['-pushed']}, | 20 | |||
), | 21 | |||
migrations.RenameField( | 22 | |||
model_name='class', | 23 | |||
old_name='professor', | 24 | |||
new_name='instructor', | 25 | |||
), | 26 | |||
migrations.RemoveField( | 27 | |||
model_name='flashcard', | 28 | |||
name='hidden', | 29 | |||
), | 30 | |||
migrations.AddField( | 31 | |||
model_name='flashcard', | 32 | |||
name='hide_reason', | 33 | |||
field=models.CharField(help_text=b'Reason for hiding this card', max_length=255, blank=True), | 34 | |||
), | 35 | |||
migrations.AddField( | 36 | |||
model_name='flashcard', | 37 | |||
name='is_hidden', | 38 | |||
field=models.BooleanField(default=False), | 39 | |||
), | 40 | |||
migrations.AlterField( | 41 | |||
model_name='flashcard', | 42 | |||
name='associated_class', | 43 | |||
field=models.ForeignKey(help_text=b'The class with which the card is associated', to='flashcards.Class'), | 44 | |||
), | 45 | |||
migrations.AlterField( | 46 | |||
model_name='flashcard', | 47 | |||
name='mask', | 48 | |||
field=models.ForeignKey(blank=True, to='flashcards.FlashcardMask', help_text=b'The default mask for this card', null=True), | 49 | |||
), | 50 | |||
migrations.AlterField( | 51 | |||
model_name='flashcard', | 52 | |||
name='material_date', | 53 | |||
field=models.DateTimeField(help_text=b'The date with which the card is associated'), | 54 | |||
), | 55 | |||
migrations.AlterField( | 56 | |||
model_name='flashcard', | 57 | |||
name='previous', | 58 | |||
field=models.ForeignKey(blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), | 59 | |||
), | 60 | |||
migrations.AlterField( | 61 | |||
model_name='flashcard', | 62 | |||
name='pushed', | 63 | |||
field=models.DateTimeField(help_text=b'When the card was first pushed', auto_now_add=True), | 64 | |||
), | 65 | |||
migrations.AlterField( | 66 | |||
model_name='flashcard', | 67 | |||
name='text', | 68 | |||
field=models.CharField(help_text=b'The text on the card', max_length=255), | 69 | |||
), | 70 | |||
migrations.AlterField( | 71 | |||
model_name='userflashcard', | 72 | |||
name='mask', | 73 | |||
field=models.ForeignKey(help_text=b"A mask which overrides the card's mask", to='flashcards.FlashcardMask'), | 74 | |||
), | 75 | |||
migrations.AlterField( | 76 | |||
model_name='userflashcard', | 77 | |||
name='pulled', | 78 | |||
field=models.DateTimeField(help_text=b'When the user pulled the card', null=True, blank=True), | 79 | |||
), | 80 | |||
migrations.AlterField( | 81 | |||
model_name='userflashcard', | 82 | |||
name='unpulled', | 83 | |||
field=models.DateTimeField(help_text=b'When the user unpulled this card', null=True, blank=True), | 84 | |||
), | 85 | |||
migrations.AlterField( | 86 | |||
model_name='userflashcardreview', | 87 | |||
name='blanked_word', | 88 | |||
field=models.CharField(help_text=b'The character range which was blanked', max_length=8, blank=True), | 89 | |||
), | 90 | |||
migrations.AlterField( | 91 | |||
model_name='userflashcardreview', | 92 | |||
name='correct', | 93 | |||
field=models.NullBooleanField(help_text=b"The user's self-evaluation of their response"), | 94 | |||
), | 95 | |||
migrations.AlterField( | 96 | |||
model_name='userflashcardreview', | 97 | |||
name='response', | 98 | |||
field=models.CharField(help_text=b"The user's response", max_length=255, null=True, blank=True), | 99 | |||
), | 100 |
flashcards/migrations/0004_auto_20150429_0827.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0003_auto_20150429_0344'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.CreateModel( | 14 | |||
name='LecturePeriod', | 15 | |||
fields=[ | 16 | |||
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), | 17 | |||
('week_day', models.IntegerField(help_text=b'0-indexed day of week, starting at Monday')), | 18 | |||
('start_time', models.TimeField()), | 19 | |||
('end_time', models.TimeField()), | 20 | |||
], | 21 | |||
), | 22 | |||
migrations.RenameModel( | 23 | |||
old_name='Class', | 24 | |||
new_name='Section', | 25 | |||
), | 26 | |||
migrations.RemoveField( | 27 | |||
model_name='flashcard', | 28 | |||
name='associated_class', | 29 | |||
), | 30 | |||
migrations.AddField( | 31 | |||
model_name='flashcard', | 32 | |||
name='section', | 33 | |||
field=models.ForeignKey(default=None, to='flashcards.Section', help_text=b'The section with which the card is associated'), | 34 | |||
preserve_default=False, | 35 | |||
), | 36 | |||
migrations.AddField( | 37 |
flashcards/migrations/0005_auto_20150430_0557.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0004_auto_20150429_0827'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.RenameField( | 14 | |||
model_name='section', | 15 | |||
old_name='name', | 16 | |||
new_name='course_title', | 17 | |||
), | 18 | |||
migrations.RenameField( | 19 | |||
model_name='section', | 20 | |||
old_name='instructor', | 21 | |||
new_name='professor', | 22 | |||
), | 23 |
flashcards/migrations/0006_auto_20150430_0643.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0005_auto_20150430_0557'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.AlterField( | 14 |
flashcards/migrations/0007_auto_20150430_0657.py
View file @
18095ed
# -*- coding: utf-8 -*- | 1 | File was deleted | ||
from __future__ import unicode_literals | 2 | |||
3 | ||||
from django.db import models, migrations | 4 | |||
5 | ||||
6 | ||||
class Migration(migrations.Migration): | 7 | |||
8 | ||||
dependencies = [ | 9 | |||
('flashcards', '0006_auto_20150430_0643'), | 10 | |||
] | 11 | |||
12 | ||||
operations = [ | 13 | |||
migrations.RenameField( | 14 | |||
model_name='section', | 15 | |||
old_name='professor', | 16 | |||
new_name='instructor', | 17 | |||
), | 18 |
flashcards/models.py
View file @
18095ed
from django.contrib.auth.models import User | 1 | 1 | from django.contrib.auth.models import AbstractBaseUser, BaseUserManager | |
2 | from django.contrib.auth.tests.custom_user import CustomUser | |||
from django.db.models import * | 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 | ||||
class UserFlashcard(Model): | 5 | 47 | class UserFlashcard(Model): | |
""" | 6 | 48 | """ | |
Represents the relationship between a user and a flashcard by: | 7 | 49 | Represents the relationship between a user and a flashcard by: | |
1. A user has a flashcard in their deck | 8 | 50 | 1. A user has a flashcard in their deck | |
2. A user used to have a flashcard in their deck | 9 | 51 | 2. A user used to have a flashcard in their deck | |
3. A user has a flashcard hidden from them | 10 | 52 | 3. A user has a flashcard hidden from them | |
""" | 11 | 53 | """ | |
user = ForeignKey(User) | 12 | 54 | user = ForeignKey(User) | |
mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask") | 13 | 55 | mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask") | |
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") | 14 | 56 | pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") | |
flashcard = ForeignKey('Flashcard') | 15 | 57 | flashcard = ForeignKey('Flashcard') | |
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") | 16 | 58 | unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") | |
17 | 59 | |||
class Meta: | 18 | 60 | class Meta: | |
# There can be at most one UserFlashcard for each User and Flashcard | 19 | 61 | # There can be at most one UserFlashcard for each User and Flashcard | |
unique_together = (('user', 'flashcard'),) | 20 | 62 | unique_together = (('user', 'flashcard'),) | |
index_together = ["user", "flashcard"] | 21 | 63 | index_together = ["user", "flashcard"] | |
# By default, order by most recently pulled | 22 | 64 | # By default, order by most recently pulled | |
ordering = ['-pulled'] | 23 | 65 | ordering = ['-pulled'] | |
24 | 66 | |||
def is_hidden(self): | 25 | 67 | def is_hidden(self): | |
""" | 26 | 68 | """ | |
A card is hidden only if a user has not ever added it to their deck. | 27 | 69 | A card is hidden only if a user has not ever added it to their deck. | |
:return: Whether the flashcard is hidden from the user | 28 | 70 | :return: Whether the flashcard is hidden from the user | |
""" | 29 | 71 | """ | |
return not self.pulled | 30 | 72 | return not self.pulled | |
31 | 73 | |||
def is_in_deck(self): | 32 | 74 | def is_in_deck(self): | |
""" | 33 | 75 | """ | |
:return:Whether the flashcard is in the user's deck | 34 | 76 | :return:Whether the flashcard is in the user's deck | |
""" | 35 | 77 | """ | |
return self.pulled and not self.unpulled | 36 | 78 | return self.pulled and not self.unpulled | |
37 | 79 | |||
38 | 80 | |||
class FlashcardMask(Model): | 39 | 81 | class FlashcardMask(Model): | |
""" | 40 | 82 | """ | |
A serialized list of character ranges that can be blanked out during review. | 41 | 83 | A serialized list of character ranges that can be blanked out during review. | |
This is encoded as '13-145,150-195' | 42 | 84 | This is encoded as '13-145,150-195' | |
""" | 43 | 85 | """ | |
ranges = CharField(max_length=255) | 44 | 86 | ranges = CharField(max_length=255) | |
45 | 87 | |||
46 | 88 | |||
class Flashcard(Model): | 47 | 89 | class Flashcard(Model): | |
text = CharField(max_length=255, help_text='The text on the card') | 48 | 90 | text = CharField(max_length=255, help_text='The text on the card') | |
section = ForeignKey('Section', help_text='The section with which the card is associated') | 49 | 91 | section = ForeignKey('Section', help_text='The section with which the card is associated') | |
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") | 50 | 92 | pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") | |
material_date = DateTimeField(help_text="The date with which the card is associated") | 51 | 93 | material_date = DateTimeField(help_text="The date with which the card is associated") | |
previous = ForeignKey('Flashcard', null=True, blank=True, | 52 | 94 | previous = ForeignKey('Flashcard', null=True, blank=True, | |
help_text="The previous version of this card, if one exists") | 53 | 95 | help_text="The previous version of this card, if one exists") | |
author = ForeignKey(User) | 54 | 96 | author = ForeignKey(User) | |
is_hidden = BooleanField(default=False) | 55 | 97 | is_hidden = BooleanField(default=False) | |
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") | 56 | 98 | hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") | |
mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") | 57 | 99 | mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") | |
58 | 100 | |||
class Meta: | 59 | 101 | class Meta: | |
# By default, order by most recently pushed | 60 | 102 | # By default, order by most recently pushed | |
ordering = ['-pushed'] | 61 | 103 | ordering = ['-pushed'] | |
62 | 104 | |||
def is_hidden_from(self, user): | 63 | 105 | def is_hidden_from(self, user): | |
""" | 64 | 106 | """ | |
A card can be hidden globally, but if a user has the card in their deck, | 65 | 107 | A card can be hidden globally, but if a user has the card in their deck, | |
this visibility overrides a global hide. | 66 | 108 | this visibility overrides a global hide. | |
:param user: | 67 | 109 | :param user: | |
:return: Whether the card is hidden from the user. | 68 | 110 | :return: Whether the card is hidden from the user. | |
""" | 69 | 111 | """ | |
result = user.userflashcard_set.filter(flashcard=self) | 70 | 112 | result = user.userflashcard_set.filter(flashcard=self) | |
if not result.exists(): return self.is_hidden | 71 | 113 | if not result.exists(): return self.is_hidden | |
return result[0].is_hidden() | 72 | 114 | return result[0].is_hidden() | |
73 | 115 | |||
74 | 116 | |||
@classmethod | 75 | 117 | @classmethod | |
def cards_visible_to(cls, user): | 76 | 118 | def cards_visible_to(cls, user): | |
""" | 77 | 119 | """ | |
:param user: | 78 | 120 | :param user: | |
:return: A queryset with all cards that should be visible to a user. | 79 | 121 | :return: A queryset with all cards that should be visible to a user. | |
""" | 80 | 122 | """ | |
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) | 81 | 123 | return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) | |
82 | 124 | |||
83 | 125 | |||
class UserFlashcardReview(Model): | 84 | 126 | class UserFlashcardReview(Model): | |
""" | 85 | 127 | """ | |
An event of a user reviewing a flashcard. | 86 | 128 | An event of a user reviewing a flashcard. | |
""" | 87 | 129 | """ | |
user_flashcard = ForeignKey(UserFlashcard) | 88 | 130 | user_flashcard = ForeignKey(UserFlashcard) | |
when = DateTimeField() | 89 | 131 | when = DateTimeField() | |
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") | 90 | 132 | blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") | |
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") | 91 | 133 | response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") | |
correct = NullBooleanField(help_text="The user's self-evaluation of their response") | 92 | 134 | correct = NullBooleanField(help_text="The user's self-evaluation of their response") | |
93 | 135 | |||
def status(self): | 94 | 136 | def status(self): | |
""" | 95 | 137 | """ | |
There are three stages of a review object: | 96 | 138 | There are three stages of a review object: | |
1. the user has been shown the card | 97 | 139 | 1. the user has been shown the card | |
2. the user has answered the card | 98 | 140 | 2. the user has answered the card | |
3. the user has self-evaluated their response's correctness | 99 | 141 | 3. the user has self-evaluated their response's correctness | |
100 | 142 | |||
:return: string (evaluated, answered, viewed) | 101 | 143 | :return: string (evaluated, answered, viewed) | |
""" | 102 | 144 | """ | |
if self.correct is not None: return "evaluated" | 103 | 145 | if self.correct is not None: return "evaluated" | |
if self.response: return "answered" | 104 | 146 | if self.response: return "answered" | |
return "viewed" | 105 | 147 | return "viewed" | |
106 | 148 | |||
149 | ||||
class Section(Model): | 107 | 150 | class Section(Model): | |
""" | 108 | 151 | """ | |
A UCSD course taught by an instructor during a quarter. | 109 | 152 | A UCSD course taught by an instructor during a quarter. | |
Different sections taught by the same instructor in the same quarter are considered identical. | 110 | 153 | Different sections taught by the same instructor in the same quarter are considered identical. | |
We use the term "section" to avoid collision with the builtin keyword "class" | 111 | 154 | We use the term "section" to avoid collision with the builtin keyword "class" | |
""" | 112 | 155 | """ | |
department = CharField(max_length=50) | 113 | 156 | department = CharField(max_length=50) |
flashcards/serializers.py
View file @
18095ed
from flashcards.models import Section, LecturePeriod | 1 | 1 | from flashcards.models import Section, LecturePeriod, User | |
2 | from rest_framework.fields import EmailField | |||
from rest_framework.relations import HyperlinkedRelatedField | 2 | 3 | from rest_framework.relations import HyperlinkedRelatedField | |
from rest_framework.serializers import HyperlinkedModelSerializer | 3 | 4 | from rest_framework.serializers import HyperlinkedModelSerializer | |
from django.contrib.auth.models import User | 4 | |||
5 | 5 | |||
6 | ||||
class SectionSerializer(HyperlinkedModelSerializer): | 6 | 7 | class SectionSerializer(HyperlinkedModelSerializer): | |
lectureperiod_set = HyperlinkedRelatedField(many=True, view_name='lectureperiod-detail', read_only=True) | 7 | 8 | lectureperiod_set = HyperlinkedRelatedField(many=True, view_name='lectureperiod-detail', read_only=True) | |
9 | ||||
class Meta: | 8 | 10 | class Meta: | |
model = Section | 9 | 11 | model = Section | |
exclude = ('members',) | 10 | 12 | exclude = ('members',) | |
11 | 13 | |||
12 | 14 | |||
class LecturePeriodSerializer(HyperlinkedModelSerializer): | 13 | 15 | class LecturePeriodSerializer(HyperlinkedModelSerializer): | |
class Meta: | 14 | 16 | class Meta: | |
model = LecturePeriod | 15 | 17 | model = LecturePeriod | |
16 | 18 | |||
19 | ||||
class UserSerializer(HyperlinkedModelSerializer): | 17 | 20 | class UserSerializer(HyperlinkedModelSerializer): | |
""" | 18 | 21 | """ | |
""" | 19 | 22 | """ | |
23 | email = EmailField(required=False) | |||
24 | ||||
class Meta: | 20 | 25 | class Meta: | |
model = User | 21 | 26 | model = User |
flashcards/views.py
View file @
18095ed
from flashcards.models import Section, LecturePeriod | 1 | 1 | from flashcards.models import Section, LecturePeriod | |
from flashcards.serializers import SectionSerializer, LecturePeriodSerializer | 2 | 2 | from flashcards.serializers import SectionSerializer, LecturePeriodSerializer | |
from rest_framework.permissions import IsAuthenticatedOrReadOnly | 3 | 3 | from rest_framework.permissions import IsAuthenticatedOrReadOnly | |
from rest_framework.viewsets import ModelViewSet | 4 | 4 | from rest_framework.viewsets import ModelViewSet | |
from rest_framework.pagination import PageNumberPagination | 5 | 5 | from rest_framework.pagination import PageNumberPagination | |
from django.core.paginator import Paginator | 6 | |||
7 | 6 | |||
7 | ||||
class StandardResultsSetPagination(PageNumberPagination): | 8 | 8 | class StandardResultsSetPagination(PageNumberPagination): | |
page_size = 40 | 9 | 9 | page_size = 40 | |
page_size_query_param = 'page_size' | 10 | 10 | page_size_query_param = 'page_size' | |
max_page_size = 1000 | 11 | 11 | max_page_size = 1000 | |
12 | 12 | |||
13 | ||||
class SectionViewSet(ModelViewSet): | 13 | 14 | class SectionViewSet(ModelViewSet): | |
queryset = Section.objects.all() | 14 | 15 | queryset = Section.objects.all() | |
serializer_class = SectionSerializer | 15 | 16 | serializer_class = SectionSerializer | |
permission_classes = (IsAuthenticatedOrReadOnly,) | 16 | 17 | permission_classes = (IsAuthenticatedOrReadOnly,) | |
pagination_class = StandardResultsSetPagination | 17 | 18 | pagination_class = StandardResultsSetPagination | |
19 | ||||
18 | 20 | |||
class LecturePeriodViewSet(ModelViewSet): | 19 | 21 | class LecturePeriodViewSet(ModelViewSet): | |
queryset = LecturePeriod.objects.all() | 20 | 22 | queryset = LecturePeriod.objects.all() |
flashy/settings.py
View file @
18095ed
""" | 1 | 1 | """ | |
Django settings for flashy project. | 2 | 2 | Django settings for flashy project. | |
3 | 3 | |||
Generated by 'django-admin startproject' using Django 1.8. | 4 | 4 | Generated by 'django-admin startproject' using Django 1.8. | |
5 | 5 | |||
For more information on this file, see | 6 | 6 | For more information on this file, see | |
https://docs.djangoproject.com/en/1.8/topics/settings/ | 7 | 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ | |
8 | 8 | |||
For the full list of settings and their values, see | 9 | 9 | For the full list of settings and their values, see | |
https://docs.djangoproject.com/en/1.8/ref/settings/ | 10 | 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ | |
""" | 11 | 11 | """ | |
12 | 12 | |||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) | 13 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) | |
import os | 14 | 14 | import os | |
15 | 15 | |||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | 16 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
17 | 17 | |||
18 | ||||
# Quick-start development settings - unsuitable for production | 19 | |||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ | 20 | |||
21 | ||||
# SECURITY WARNING: keep the secret key used in production secret! | 22 | |||
SECRET_KEY = '8)#-j&8dghe-y&v4a%r6u#y@r86&6nfv%k$(a((r-zh$m3ct!9' | 23 | |||
24 | ||||
# SECURITY WARNING: don't run with debug turned on in production! | 25 | 18 | # SECURITY WARNING: don't run with debug turned on in production! | |
DEBUG = True | 26 | 19 | DEBUG = True | |
27 | 20 | |||
ALLOWED_HOSTS = [] | 28 | 21 | ALLOWED_HOSTS = [] | |
29 | 22 | |||
30 | 23 | AUTH_USER_MODEL = 'flashcards.User' | ||
# Application definition | 31 | 24 | # Application definition | |
32 | 25 | |||
INSTALLED_APPS = ( | 33 | 26 | INSTALLED_APPS = ( | |
27 | 'simple_email_confirmation', | |||
28 | 'flashcards', | |||
'django.contrib.admin', | 34 | 29 | 'django.contrib.admin', | |
'django.contrib.admindocs', | 35 | 30 | 'django.contrib.admindocs', | |
'django.contrib.auth', | 36 | 31 | 'django.contrib.auth', | |
'django.contrib.contenttypes', | 37 | 32 | 'django.contrib.contenttypes', | |
'django.contrib.sessions', | 38 | 33 | 'django.contrib.sessions', | |
'django.contrib.messages', | 39 | 34 | 'django.contrib.messages', | |
'django.contrib.staticfiles', | 40 | 35 | 'django.contrib.staticfiles', | |
36 | 'django_ses', | |||
'rest_framework', | 41 | 37 | 'rest_framework', | |
'flashcards', | 42 | 38 | ||
39 | ||||
) | 43 | 40 | ) | |
44 | 41 | |||
REST_FRAMEWORK = { | 45 | 42 | REST_FRAMEWORK = { | |
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', | 46 | 43 | 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', | |
'PAGE_SIZE': 20 | 47 | 44 | 'PAGE_SIZE': 20 | |
} | 48 | 45 | } | |
49 | 46 | |||
MIDDLEWARE_CLASSES = ( | 50 | 47 | MIDDLEWARE_CLASSES = ( | |
'django.contrib.sessions.middleware.SessionMiddleware', | 51 | 48 | 'django.contrib.sessions.middleware.SessionMiddleware', | |
'django.middleware.common.CommonMiddleware', | 52 | 49 | 'django.middleware.common.CommonMiddleware', | |
'django.middleware.csrf.CsrfViewMiddleware', | 53 | 50 | 'django.middleware.csrf.CsrfViewMiddleware', | |
'django.contrib.auth.middleware.AuthenticationMiddleware', | 54 | 51 | 'django.contrib.auth.middleware.AuthenticationMiddleware', | |
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | 55 | 52 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', | |
'django.contrib.messages.middleware.MessageMiddleware', | 56 | 53 | 'django.contrib.messages.middleware.MessageMiddleware', | |
'django.middleware.clickjacking.XFrameOptionsMiddleware', | 57 | 54 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', | |
'django.middleware.security.SecurityMiddleware', | 58 | 55 | 'django.middleware.security.SecurityMiddleware', | |
) | 59 | 56 | ) | |
60 | 57 | |||
ROOT_URLCONF = 'flashy.urls' | 61 | 58 | ROOT_URLCONF = 'flashy.urls' | |
62 | 59 | |||
TEMPLATES = [ | 63 | 60 | TEMPLATES = [ | |
{ | 64 | 61 | { | |
'BACKEND': 'django.template.backends.django.DjangoTemplates', | 65 | 62 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', | |
'DIRS': ['templates/'], | 66 | 63 | 'DIRS': ['templates/'], | |
'APP_DIRS': True, | 67 | 64 | 'APP_DIRS': True, | |
'OPTIONS': { | 68 | 65 | 'OPTIONS': { | |
'context_processors': [ | 69 | 66 | 'context_processors': [ | |
'django.template.context_processors.debug', | 70 | 67 | 'django.template.context_processors.debug', | |
'django.template.context_processors.request', | 71 | 68 | 'django.template.context_processors.request', | |
'django.contrib.auth.context_processors.auth', | 72 | 69 | 'django.contrib.auth.context_processors.auth', | |
'django.contrib.messages.context_processors.messages', | 73 | 70 | 'django.contrib.messages.context_processors.messages', | |
], | 74 | 71 | ], | |
}, | 75 | 72 | }, | |
}, | 76 | 73 | }, | |
] | 77 | 74 | ] | |
78 | 75 | |||
WSGI_APPLICATION = 'flashy.wsgi.application' | 79 | 76 | WSGI_APPLICATION = 'flashy.wsgi.application' | |
80 | 77 | |||
81 | 78 | |||
# Database | 82 | 79 | # Database | |
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases | 83 | 80 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases | |
84 | 81 | |||
DATABASES = { | 85 | 82 | DATABASES = { | |
'default': { | 86 | 83 | 'default': { |
requirements.txt
View file @
18095ed
beautifulsoup4 | 1 | 1 | beautifulsoup4 | |
Django>=1.8 | 2 | 2 | Django>=1.8 | |
#django-websocket-redis==0.4.3 | 3 | 3 | #django-websocket-redis==0.4.3 | |
#gevent==1.0.1 | 4 | 4 | #gevent==1.0.1 | |
#greenlet==0.4.5 | 5 | 5 | #greenlet==0.4.5 | |
#redis==2.10.3 | 6 | 6 | #redis==2.10.3 | |
six==1.9.0 | 7 | 7 | six==1.9.0 | |
djangorestframework | 8 | 8 | djangorestframework | |
docutils | 9 | 9 | docutils | |
gunicorn | 10 | 10 | gunicorn | |
11 | django-simple-email-confirmation | |||
12 | django-ses |