Commit 9f4aa9bfaa00c0a552111047a025a0372c43f574

Authored by Andrew Buss
1 parent 5dce065517
Exists in master

Added FlashcardReport and WhitelistedAddress to support use cases\nRegistration …

…now actually sends an email

Showing 2 changed files with 29 additions and 5 deletions Inline Diff

flashcards/api.py View file @ 9f4aa9b
from django.core.mail import send_mail 1 1 from django.core.mail import send_mail
from django.contrib.auth import authenticate, login, logout 2 2 from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.tokens import default_token_generator 3 3 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_201_CREATED, HTTP_204_NO_CONTENT 4 4 from rest_framework.status import HTTP_201_CREATED, HTTP_204_NO_CONTENT
from rest_framework.views import APIView 5 5 from rest_framework.views import APIView
from rest_framework.response import Response 6 6 from rest_framework.response import Response
from rest_framework import status 7 7 from rest_framework import status
from rest_framework.exceptions import ValidationError, NotFound 8 8 from rest_framework.exceptions import ValidationError, NotFound
from flashcards.serializers import * 9 9 from flashcards.serializers import *
10 10
11 11
class UserDetail(APIView): 12 12 class UserDetail(APIView):
def patch(self, request, format=None): 13 13 def patch(self, request, format=None):
""" 14 14 """
This method checks either the email or the password passed in 15 15 This method checks either the email or the password passed in
is valid. If confirmation key is correct, it validates the 16 16 is valid. If confirmation key is correct, it validates the
user. It updates the password if the new password 17 17 user. It updates the password if the new password
is valid. 18 18 is valid.
19 19
""" 20 20 """
currentuser = request.user 21 21 currentuser = request.user
22 22
if 'confirmation_key' in request.data: 23 23 if 'confirmation_key' in request.data:
if not currentuser.confirm_email( request.data['confirmation_key'] ): 24 24 if not currentuser.confirm_email(request.data['confirmation_key']):
raise ValidationError('confirmation_key is invalid') 25 25 raise ValidationError('confirmation_key is invalid')
26 26
if 'new_password' in request.data: 27 27 if 'new_password' in request.data:
if not currentuser.check_password(request.data['old_password']): 28 28 if not currentuser.check_password(request.data['old_password']):
raise ValidationError('Invalid old password') 29 29 raise ValidationError('Invalid old password')
if not request.data['new_password']: 30 30 if not request.data['new_password']:
raise ValidationError('Password cannot be blank') 31 31 raise ValidationError('Password cannot be blank')
currentuser.set_password(request.data['new_password']) 32 32 currentuser.set_password(request.data['new_password'])
currentuser.save() 33 33 currentuser.save()
34 34
return Response(status=status.HTTP_204_NO_CONTENT) 35 35 return Response(status=status.HTTP_204_NO_CONTENT)
36 36
def get(self, request, format=None): 37 37 def get(self, request, format=None):
serializer = UserSerializer(request.user) 38 38 serializer = UserSerializer(request.user)
return Response(serializer.data) 39 39 return Response(serializer.data)
40 40
def post(self, request, format=None): 41 41 def post(self, request, format=None):
if 'email' not in request.data: 42 42 if 'email' not in request.data:
raise ValidationError('Email is required') 43 43 raise ValidationError('Email is required')
if 'password' not in request.data: 44 44 if 'password' not in request.data:
raise ValidationError('Password is required') 45 45 raise ValidationError('Password is required')
email = request.data['email'] 46 46 email = request.data['email']
existing_users = User.objects.filter(email=email) 47 47 existing_users = User.objects.filter(email=email)
if existing_users.exists(): 48 48 if existing_users.exists():
raise ValidationError("An account with this email already exists") 49 49 raise ValidationError("An account with this email already exists")
user = User.objects.create_user(email, email=email, password=request.data['password']) 50 50 user = User.objects.create_user(email, email=email, password=request.data['password'])
51 51
body = ''' 52 52 body = '''
Visit the following link to confirm your email address: 53 53 Visit the following link to confirm your email address:
http://flashy.cards/app/verify_email/%s 54 54 http://flashy.cards/app/verify_email/%s
55 55
If you did not register for Flashy, no action is required. 56 56 If you did not register for Flashy, no action is required.
''' 57 57 '''
58 send_mail("Flashy email verification",
59 body % (user.pk, user.confirmation_key),
60 "noreply@flashy.cards",
61 [user.email])
58 62
user = authenticate(email=email, password=request.data['password']) 59 63 user = authenticate(email=email, password=request.data['password'])
login(request, user) 60 64 login(request, user)
return Response(UserSerializer(user).data, status=HTTP_201_CREATED) 61 65 return Response(UserSerializer(user).data, status=HTTP_201_CREATED)
62 66
def delete(self, request): 63 67 def delete(self, request):
request.user.delete() 64 68 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 65 69 return Response(status=HTTP_204_NO_CONTENT)
66 70
67 71
class UserLogin(APIView): 68 72 class UserLogin(APIView):
""" 69 73 """
Authenticates user and returns user data if valid. Handles invalid 70 74 Authenticates user and returns user data if valid. Handles invalid
users. 71 75 users.
""" 72 76 """
73 77
def post(self, request, format=None): 74 78 def post(self, request, format=None):
""" 75 79 """
Returns user data if valid. 76 80 Returns user data if valid.
""" 77 81 """
if 'email' not in request.data: 78 82 if 'email' not in request.data:
raise ValidationError('Email is required') 79 83 raise ValidationError('Email is required')
if 'password' not in request.data: 80 84 if 'password' not in request.data:
raise ValidationError('Password is required') 81 85 raise ValidationError('Password is required')
82 86
email = request.data['email'] 83 87 email = request.data['email']
password = request.data['password'] 84 88 password = request.data['password']
user = authenticate(email=email, password=password) 85 89 user = authenticate(email=email, password=password)
86 90
if user is None: 87 91 if user is None:
raise ValidationError('Invalid email or password') 88 92 raise ValidationError('Invalid email or password')
if not user.is_active: 89 93 if not user.is_active:
raise ValidationError('Account is disabled') 90 94 raise ValidationError('Account is disabled')
login(request, user) 91 95 login(request, user)
return Response(UserSerializer(user).data) 92 96 return Response(UserSerializer(user).data)
97
93 98
class UserLogout(APIView): 94 99 class UserLogout(APIView):
""" 95 100 """
Logs out an authenticated user. 96 101 Logs out an authenticated user.
""" 97 102 """
98 103
def post(self, request, format=None): 99 104 def post(self, request, format=None):
logout(request, request.user) 100 105 logout(request, request.user)
return Response(status=status.HTTP_204_NO_CONTENT) 101 106 return Response(status=status.HTTP_204_NO_CONTENT)
102 107
103 108
class PasswordReset(APIView): 104 109 class PasswordReset(APIView):
""" 105 110 """
Allows user to reset their password. 106 111 Allows user to reset their password.
""" 107 112 """
108 113
def post(self, request, format=None): 109 114 def post(self, request, format=None):
""" 110 115 """
Send a password reset token/link to the provided email. 111 116 Send a password reset token/link to the provided email.
""" 112 117 """
if 'email' not in request.data: 113 118 if 'email' not in request.data:
raise ValidationError('Email is required') 114 119 raise ValidationError('Email is required')
115 120
email = request.data['email'] 116 121 email = request.data['email']
117 122
# Find the user since they are not logged in. 118 123 # Find the user since they are not logged in.
try: 119 124 try:
user = User.objects.get(email=email) 120 125 user = User.objects.get(email=email)
except User.DoesNotExist: 121 126 except User.DoesNotExist:
raise NotFound('Email does not exist') 122 127 raise NotFound('Email does not exist')
123 128
token = default_token_generator.make_token(user) 124 129 token = default_token_generator.make_token(user)
flashcards/models.py View file @ 9f4aa9b
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin, AbstractUser 1 1 from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser
from django.db.models import * 2 2 from django.db.models import *
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 3 3 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
4 4
# Hack to fix AbstractUser before subclassing it 5 5 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 6 6 AbstractUser._meta.get_field('email')._unique = True
7 7
8
class User(AbstractUser, SimpleEmailConfirmationUserMixin, ): 8 9 class User(AbstractUser, SimpleEmailConfirmationUserMixin, ):
USERNAME_FIELD = 'email' 9 10 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 10 11 REQUIRED_FIELDS = []
sections = ManyToManyField('Section') 11 12 sections = ManyToManyField('Section')
12 13
13 14
class UserFlashcard(Model): 14 15 class UserFlashcard(Model):
""" 15 16 """
Represents the relationship between a user and a flashcard by: 16 17 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 17 18 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 18 19 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 19 20 3. A user has a flashcard hidden from them
""" 20 21 """
user = ForeignKey('User') 21 22 user = ForeignKey('User')
mask = ForeignKey('FlashcardMask', help_text="A mask which overrides the card's mask") 22 23 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") 23 24 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 24 25 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 25 26 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
26 27
class Meta: 27 28 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 28 29 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 29 30 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 30 31 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 31 32 # By default, order by most recently pulled
ordering = ['-pulled'] 32 33 ordering = ['-pulled']
33 34
def is_hidden(self): 34 35 def is_hidden(self):
""" 35 36 """
A card is hidden only if a user has not ever added it to their deck. 36 37 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 37 38 :return: Whether the flashcard is hidden from the user
""" 38 39 """
return not self.pulled 39 40 return not self.pulled
40 41
def is_in_deck(self): 41 42 def is_in_deck(self):
""" 42 43 """
:return:Whether the flashcard is in the user's deck 43 44 :return:Whether the flashcard is in the user's deck
""" 44 45 """
return self.pulled and not self.unpulled 45 46 return self.pulled and not self.unpulled
46 47
47 48
class FlashcardMask(Model): 48 49 class FlashcardMask(Model):
""" 49 50 """
A serialized list of character ranges that can be blanked out during review. 50 51 A serialized list of character ranges that can be blanked out during review.
This is encoded as '13-145,150-195' 51 52 This is encoded as '13-145,150-195'
""" 52 53 """
ranges = CharField(max_length=255) 53 54 ranges = CharField(max_length=255)
54 55
55 56
class Flashcard(Model): 56 57 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 57 58 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') 58 59 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") 59 60 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") 60 61 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 61 62 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 62 63 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 63 64 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 64 65 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 65 66 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") 66 67 mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card")
67 68
class Meta: 68 69 class Meta:
# By default, order by most recently pushed 69 70 # By default, order by most recently pushed
ordering = ['-pushed'] 70 71 ordering = ['-pushed']
71 72
def is_hidden_from(self, user): 72 73 def is_hidden_from(self, user):
""" 73 74 """
A card can be hidden globally, but if a user has the card in their deck, 74 75 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 75 76 this visibility overrides a global hide.
:param user: 76 77 :param user:
:return: Whether the card is hidden from the user. 77 78 :return: Whether the card is hidden from the user.
""" 78 79 """
result = user.userflashcard_set.filter(flashcard=self) 79 80 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 80 81 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 81 82 return result[0].is_hidden()
82 83
83 84
@classmethod 84 85 @classmethod
def cards_visible_to(cls, user): 85 86 def cards_visible_to(cls, user):
""" 86 87 """
:param user: 87 88 :param user:
:return: A queryset with all cards that should be visible to a user. 88 89 :return: A queryset with all cards that should be visible to a user.
""" 89 90 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 90 91 return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None)
91 92
92 93
class UserFlashcardReview(Model): 93 94 class UserFlashcardReview(Model):
""" 94 95 """
An event of a user reviewing a flashcard. 95 96 An event of a user reviewing a flashcard.
""" 96 97 """
user_flashcard = ForeignKey(UserFlashcard) 97 98 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 98 99 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 99 100 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") 100 101 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") 101 102 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
102 103
def status(self): 103 104 def status(self):
""" 104 105 """
There are three stages of a review object: 105 106 There are three stages of a review object:
1. the user has been shown the card 106 107 1. the user has been shown the card
2. the user has answered the card 107 108 2. the user has answered the card
3. the user has self-evaluated their response's correctness 108 109 3. the user has self-evaluated their response's correctness
109 110
:return: string (evaluated, answered, viewed) 110 111 :return: string (evaluated, answered, viewed)
""" 111 112 """
if self.correct is not None: return "evaluated" 112 113 if self.correct is not None: return "evaluated"
if self.response: return "answered" 113 114 if self.response: return "answered"
return "viewed" 114 115 return "viewed"
115 116
116 117
class Section(Model): 117 118 class Section(Model):
""" 118 119 """
A UCSD course taught by an instructor during a quarter. 119 120 A UCSD course taught by an instructor during a quarter.
Different sections taught by the same instructor in the same quarter are considered identical. 120 121 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" 121 122 We use the term "section" to avoid collision with the builtin keyword "class"
""" 122 123 """
department = CharField(max_length=50) 123 124 department = CharField(max_length=50)
course_num = CharField(max_length=6) 124 125 course_num = CharField(max_length=6)
# section_id = CharField(max_length=10) 125 126 # section_id = CharField(max_length=10)
course_title = CharField(max_length=50) 126 127 course_title = CharField(max_length=50)
instructor = CharField(max_length=50) 127 128 instructor = CharField(max_length=50)
quarter = CharField(max_length=4) 128 129 quarter = CharField(max_length=4)
130 whitelist = ManyToManyField(User, related_name="whitelisted_sections")
129 131
class Meta: 130 132 class Meta:
unique_together = (('department', 'course_num', 'quarter', 'instructor'),) 131 133 unique_together = (('department', 'course_num', 'quarter', 'instructor'),)
ordering = ['-quarter'] 132 134 ordering = ['-quarter']
133 135
134 136
class LecturePeriod(Model): 135 137 class LecturePeriod(Model):
""" 136 138 """