Commit 57168cbc293485da632575f11e89ef42c793e0fd

Authored by Rachel Lee
1 parent 2f1be78a16
Exists in master

Stuff

Showing 2 changed files with 12 additions and 1 deletions Inline Diff

flashcards/models.py View file @ 57168cb
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.db.models import * 2 2 from django.db.models import *
from django.utils.timezone import now 3 3 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 4 4 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 5 5 from fields import MaskField
6 6
# Hack to fix AbstractUser before subclassing it 7 7 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 8 8 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 9 9 AbstractUser._meta.get_field('username')._unique = False
10 10
11 11
class EmailOnlyUserManager(UserManager): 12 12 class EmailOnlyUserManager(UserManager):
""" 13 13 """
A tiny extension of Django's UserManager which correctly creates users 14 14 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 15 15 without usernames (using emails instead).
""" 16 16 """
17 17
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 18 18 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 19 19 """
Creates and saves a User with the given email and password. 20 20 Creates and saves a User with the given email and password.
""" 21 21 """
email = self.normalize_email(email) 22 22 email = self.normalize_email(email)
user = self.model(email=email, 23 23 user = self.model(email=email,
is_staff=is_staff, is_active=True, 24 24 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 25 25 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 26 26 date_joined=now(), **extra_fields)
user.set_password(password) 27 27 user.set_password(password)
user.save(using=self._db) 28 28 user.save(using=self._db)
return user 29 29 return user
30 30
def create_user(self, email, password=None, **extra_fields): 31 31 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 32 32 return self._create_user(email, password, False, False, **extra_fields)
33 33
def create_superuser(self, email, password, **extra_fields): 34 34 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 35 35 return self._create_user(email, password, True, True, **extra_fields)
36 36
37 37
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 38 38 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 39 39 """
An extension of Django's default user model. 40 40 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 41 41 We use email as the username field, and include enrolled sections here
""" 42 42 """
objects = EmailOnlyUserManager() 43 43 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 44 44 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 45 45 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 46 46 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
47 47
48 48
class UserFlashcard(Model): 49 49 class UserFlashcard(Model):
""" 50 50 """
Represents the relationship between a user and a flashcard by: 51 51 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 52 52 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 53 53 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 54 54 3. A user has a flashcard hidden from them
""" 55 55 """
user = ForeignKey('User') 56 56 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") 57 57 mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") 58 58 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 59 59 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 60 60 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
61 61
class Meta: 62 62 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 63 63 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 64 64 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 65 65 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 66 66 # By default, order by most recently pulled
ordering = ['-pulled'] 67 67 ordering = ['-pulled']
68 68
def is_hidden(self): 69 69 def is_hidden(self):
""" 70 70 """
A card is hidden only if a user has not ever added it to their deck. 71 71 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 72 72 :return: Whether the flashcard is hidden from the user
""" 73 73 """
return not self.pulled 74 74 return not self.pulled
75 75
def is_in_deck(self): 76 76 def is_in_deck(self):
""" 77 77 """
:return:Whether the flashcard is in the user's deck 78 78 :return:Whether the flashcard is in the user's deck
""" 79 79 """
return self.pulled and not self.unpulled 80 80 return self.pulled and not self.unpulled
81 81
82 82
class Flashcard(Model): 83 83 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 84 84 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') 85 85 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") 86 86 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") 87 87 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 88 88 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 89 89 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 90 90 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 91 91 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 92 92 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 93 93 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
94 94
class Meta: 95 95 class Meta:
# By default, order by most recently pushed 96 96 # By default, order by most recently pushed
ordering = ['-pushed'] 97 97 ordering = ['-pushed']
98 98
def is_hidden_from(self, user): 99 99 def is_hidden_from(self, user):
""" 100 100 """
A card can be hidden globally, but if a user has the card in their deck, 101 101 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 102 102 this visibility overrides a global hide.
:param user: 103 103 :param user:
:return: Whether the card is hidden from the user. 104 104 :return: Whether the card is hidden from the user.
""" 105 105 """
result = user.userflashcard_set.filter(flashcard=self) 106 106 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 107 107 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 108 108 return result[0].is_hidden()
109 109
@classmethod 110 110 @classmethod
def cards_visible_to(cls, user): 111 111 def cards_visible_to(cls, user):
""" 112 112 """
:param user: 113 113 :param user:
:return: A queryset with all cards that should be visible to a user. 114 114 :return: A queryset with all cards that should be visible to a user.
""" 115 115 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 116 116 return cls.objects.filter(is_hidden=False).exclude(userflashcard__user=user, userflashcard__pulled=None)
117 117
118 118
class UserFlashcardQuiz(Model): 119 119 class UserFlashcardQuiz(Model):
""" 120 120 """
An event of a user being quizzed on a flashcard. 121 121 An event of a user being quizzed on a flashcard.
""" 122 122 """
user_flashcard = ForeignKey(UserFlashcard) 123 123 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 124 124 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 125 125 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") 126 126 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") 127 127 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
128 128
def status(self): 129 129 def status(self):
""" 130 130 """
There are three stages of a quiz object: 131 131 There are three stages of a quiz object:
1. the user has been shown the card 132 132 1. the user has been shown the card
2. the user has answered the card 133 133 2. the user has answered the card
3. the user has self-evaluated their response's correctness 134 134 3. the user has self-evaluated their response's correctness
135 135
:return: string (evaluated, answered, viewed) 136 136 :return: string (evaluated, answered, viewed)
""" 137 137 """
if self.correct is not None: return "evaluated" 138 138 if self.correct is not None: return "evaluated"
if self.response: return "answered" 139 139 if self.response: return "answered"
return "viewed" 140 140 return "viewed"
141 141
142 142
class Section(Model): 143 143 class Section(Model):
""" 144 144 """
A UCSD course taught by an instructor during a quarter. 145 145 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 146 146 We use the term "section" to avoid collision with the builtin keyword "class"
""" 147 147 """
department = CharField(db_index=True, max_length=50) 148 148 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 149 149 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 150 150 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 151 151 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 152 152 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 153 153 quarter = CharField(db_index=True, max_length=4)
154 154
def is_whitelisted(self): 155 155 def is_whitelisted(self):
""" 156 156 """
:return: whether a whitelist exists for this section 157 157 :return: whether a whitelist exists for this section
""" 158 158 """
return self.whitelist.exists() 159 159 return self.whitelist.exists()
160 160
def is_user_on_whitelist(self, user): 161 161 def is_user_on_whitelist(self, user):
""" 162 162 """
:return: whether the user is on the waitlist for this section 163 163 :return: whether the user is on the waitlist for this section
""" 164 164 """
return self.whitelist.filter(email=user.email).exists() 165 165 return self.whitelist.filter(email=user.email).exists()
166 166
class Meta: 167 167 class Meta:
ordering = ['-course_title'] 168 168 ordering = ['-course_title']
169 169
def __unicode__(self): 170 170 def __unicode__(self):
flashcards/views.py View file @ 57168cb
from django.contrib import auth 1 1 from django.contrib import auth
from django.db.models import Q 2 2 from django.db.models import Q
from flashcards.api import StandardResultsSetPagination 3 3 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 4 4 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 5 5 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 6 6 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 7 7 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 8 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 9 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 10 10 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 11 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 12 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 13 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 14 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 15 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 16 16 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 17 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 18 from simple_email_confirmation import EmailAddress
19 19
20 20
class SectionViewSet(ReadOnlyModelViewSet): 21 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 24 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 25 permission_classes = [IsAuthenticated]
26 26
27 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
28 def flashcards(self, request, pk):
29 """
30 Gets flashcards for a section, excluding hidden cards.
31 Returned in strictly chronological order (material date).
32 """
33 flashcards = Flashcard.cards_visible_to(request.user).filter( \
34 section=self.get_object(), is_hidden=False).all()
35
36 return Response(FlashcardSerializer(flashcards, many=True))
37
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 27 38 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 28 39 def enroll(self, request, pk):
""" 29 40 """
Add the current user to a specified section 30 41 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 31 42 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 32 43 ---
omit_serializer: true 33 44 omit_serializer: true
parameters: 34 45 parameters:
- fake: None 35 46 - fake: None
parameters_strategy: 36 47 parameters_strategy:
form: replace 37 48 form: replace
""" 38 49 """
section = self.get_object() 39 50 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 40 51 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 41 52 raise ValidationError("You are already in this section.")
if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): 42 53 if section.is_whitelisted() and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 43 54 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 44 55 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 45 56 return Response(status=HTTP_204_NO_CONTENT)
46 57
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 47 58 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 48 59 def drop(self, request, pk):
""" 49 60 """
Remove the current user from a specified section 50 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 51 62 If the user is not in the class, the request will fail.
--- 52 63 ---
omit_serializer: true 53 64 omit_serializer: true
parameters: 54 65 parameters:
- fake: None 55 66 - fake: None
parameters_strategy: 56 67 parameters_strategy:
form: replace 57 68 form: replace
""" 58 69 """
section = self.get_object() 59 70 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 60 71 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 61 72 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 62 73 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 63 74 return Response(status=HTTP_204_NO_CONTENT)
64 75
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 65 76 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 66 77 def search(self, request):
query = request.GET.get('q', '').split(' ') 67 78 query = request.GET.get('q', '').split(' ')
q = Q() 68 79 q = Q()
for word in query: 69 80 for word in query:
q |= Q(course_title__icontains=word) 70 81 q |= Q(course_title__icontains=word)
qs = Section.objects.filter(q).distinct() 71 82 qs = Section.objects.filter(q).distinct()
serializer = SectionSerializer(qs, many=True) 72 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 73 84 return Response(serializer.data)
74 85
75 86
class UserSectionListView(ListAPIView): 76 87 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 77 88 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 78 89 permission_classes = [IsAuthenticated]
79 90
def get_queryset(self): 80 91 def get_queryset(self):
return self.request.user.sections.all() 81 92 return self.request.user.sections.all()
82 93
def paginate_queryset(self, queryset): return None 83 94 def paginate_queryset(self, queryset): return None
84 95
85 96
class UserDetail(GenericAPIView): 86 97 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 87 98 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 88 99 permission_classes = [IsAuthenticated]
89 100
def get_queryset(self): 90 101 def get_queryset(self):
return User.objects.all() 91 102 return User.objects.all()
92 103
def patch(self, request, format=None): 93 104 def patch(self, request, format=None):
""" 94 105 """
Updates the user's password, or verifies their email address 95 106 Updates the user's password, or verifies their email address
--- 96 107 ---
request_serializer: UserUpdateSerializer 97 108 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 98 109 response_serializer: UserSerializer
""" 99 110 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 100 111 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 101 112 data.is_valid(raise_exception=True)
data = data.validated_data 102 113 data = data.validated_data
103 114
if 'new_password' in data: 104 115 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 105 116 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 106 117 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 107 118 request.user.set_password(data['new_password'])
request.user.save() 108 119 request.user.save()
109 120
if 'confirmation_key' in data: 110 121 if 'confirmation_key' in data:
try: 111 122 try:
request.user.confirm_email(data['confirmation_key']) 112 123 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 113 124 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 114 125 raise ValidationError('confirmation_key is invalid')
115 126
return Response(UserSerializer(request.user).data) 116 127 return Response(UserSerializer(request.user).data)
117 128
def get(self, request, format=None): 118 129 def get(self, request, format=None):
""" 119 130 """
Return data about the user 120 131 Return data about the user
--- 121 132 ---
response_serializer: UserSerializer 122 133 response_serializer: UserSerializer
""" 123 134 """
serializer = UserSerializer(request.user, context={'request': request}) 124 135 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 125 136 return Response(serializer.data)
126 137
def delete(self, request): 127 138 def delete(self, request):
""" 128 139 """
Irrevocably delete the user and their data 129 140 Irrevocably delete the user and their data
130 141
Yes, really 131 142 Yes, really
""" 132 143 """
request.user.delete() 133 144 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 134 145 return Response(status=HTTP_204_NO_CONTENT)
135 146
136 147
@api_view(['POST']) 137 148 @api_view(['POST'])
def register(request, format=None): 138 149 def register(request, format=None):
""" 139 150 """
Register a new user 140 151 Register a new user
--- 141 152 ---
request_serializer: EmailPasswordSerializer 142 153 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 143 154 response_serializer: UserSerializer
""" 144 155 """
data = RegistrationSerializer(data=request.data) 145 156 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 146 157 data.is_valid(raise_exception=True)
147 158
User.objects.create_user(**data.validated_data) 148 159 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 149 160 user = authenticate(**data.validated_data)
auth.login(request, user) 150 161 auth.login(request, user)
151 162
body = ''' 152 163 body = '''
Visit the following link to confirm your email address: 153 164 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 154 165 https://flashy.cards/app/verify_email/%s
155 166
If you did not register for Flashy, no action is required. 156 167 If you did not register for Flashy, no action is required.
''' 157 168 '''
158 169
assert send_mail("Flashy email verification", 159 170 assert send_mail("Flashy email verification",
body % user.confirmation_key, 160 171 body % user.confirmation_key,
"noreply@flashy.cards", 161 172 "noreply@flashy.cards",
[user.email]) 162 173 [user.email])
163 174
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 164 175 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
165 176
166 177
@api_view(['POST']) 167 178 @api_view(['POST'])
def login(request): 168 179 def login(request):
""" 169 180 """
Authenticates user and returns user data if valid. 170 181 Authenticates user and returns user data if valid.
--- 171 182 ---
request_serializer: EmailPasswordSerializer 172 183 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 173 184 response_serializer: UserSerializer
""" 174 185 """
175 186
data = EmailPasswordSerializer(data=request.data) 176 187 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 177 188 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 178 189 user = authenticate(**data.validated_data)
179 190
if user is None: 180 191 if user is None:
raise AuthenticationFailed('Invalid email or password') 181 192 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 182 193 if not user.is_active:
raise NotAuthenticated('Account is disabled') 183 194 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 184 195 auth.login(request, user)
return Response(UserSerializer(request.user).data) 185 196 return Response(UserSerializer(request.user).data)
186 197
187 198
@api_view(['POST']) 188 199 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 189 200 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 190 201 def logout(request, format=None):
""" 191 202 """
Logs the authenticated user out. 192 203 Logs the authenticated user out.
""" 193 204 """
auth.logout(request) 194 205 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 195 206 return Response(status=HTTP_204_NO_CONTENT)
196 207
197 208
@api_view(['POST']) 198 209 @api_view(['POST'])
def request_password_reset(request, format=None): 199 210 def request_password_reset(request, format=None):
""" 200 211 """
Send a password reset token/link to the provided email. 201 212 Send a password reset token/link to the provided email.
--- 202 213 ---
request_serializer: PasswordResetRequestSerializer 203 214 request_serializer: PasswordResetRequestSerializer
""" 204 215 """
data = PasswordResetRequestSerializer(data=request.data) 205 216 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 206 217 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 207 218 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 208 219 token = default_token_generator.make_token(user)
209 220
body = ''' 210 221 body = '''
Visit the following link to reset your password: 211 222 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 212 223 https://flashy.cards/app/reset_password/%d/%s
213 224
If you did not request a password reset, no action is required. 214 225 If you did not request a password reset, no action is required.
''' 215 226 '''
216 227
send_mail("Flashy password reset", 217 228 send_mail("Flashy password reset",
body % (user.pk, token), 218 229 body % (user.pk, token),
"noreply@flashy.cards", 219 230 "noreply@flashy.cards",
[user.email]) 220 231 [user.email])
221 232
return Response(status=HTTP_204_NO_CONTENT) 222 233 return Response(status=HTTP_204_NO_CONTENT)
223 234
224 235
@api_view(['POST']) 225 236 @api_view(['POST'])
def reset_password(request, format=None): 226 237 def reset_password(request, format=None):
""" 227 238 """
Updates user's password to new password if token is valid. 228 239 Updates user's password to new password if token is valid.
--- 229 240 ---
request_serializer: PasswordResetSerializer 230 241 request_serializer: PasswordResetSerializer
""" 231 242 """
data = PasswordResetSerializer(data=request.data) 232 243 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 233 244 data.is_valid(raise_exception=True)