Commit 6158afb2d52fff8df41463d92b60be3a0e884885

Authored by Laura Hawkins
1 parent 7916bfa7a6
Exists in master

my deckview is perfect and wonderful and <3 <3 <3

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

flashcards/models.py View file @ 6158afb
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(default=now, help_text="The date with which the card is associated") 87 87 material_date = DateTimeField(default=now, 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(is_hidden=False).exclude(userflashcard__user=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(auto_now=True) 124 124 when = DateTimeField(auto_now=True)
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"
We index gratuitously to support autofill and because this is primarily read-only 147 147 We index gratuitously to support autofill and because this is primarily read-only
""" 148 148 """
department = CharField(db_index=True, max_length=50) 149 149 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 150 150 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 151 151 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 152 152 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 153 153 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 154 154 quarter = CharField(db_index=True, max_length=4)
155 155
@classmethod 156 156 @classmethod
def search(cls, terms): 157 157 def search(cls, terms):
""" 158 158 """
Search all fields of all sections for a particular set of terms 159 159 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 160 160 A matching section must match at least one field on each term
:param terms:iterable 161 161 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 162 162 :return: Matching QuerySet ordered by department and course number
""" 163 163 """
final_q = Q() 164 164 final_q = Q()
for term in terms: 165 165 for term in terms:
q = Q(department__icontains=term) 166 166 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 167 167 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 168 168 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 169 169 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 170 170 q |= Q(instructor__icontains=term)
final_q &= q 171 171 final_q &= q
qs = cls.objects.filter(final_q) 172 172 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 173 173 # Have the database cast the course number to an integer so it will sort properly
# ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically 174 174 # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) 175 175 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 176 176 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 177 177 return qs
178 178
@property 179 179 @property
def is_whitelisted(self): 180 180 def is_whitelisted(self):
""" 181 181 """
:return: whether a whitelist exists for this section 182 182 :return: whether a whitelist exists for this section
""" 183 183 """
return self.whitelist.exists() 184 184 return self.whitelist.exists()
185 185
def is_user_on_whitelist(self, user): 186 186 def is_user_on_whitelist(self, user):
""" 187 187 """
:return: whether the user is on the waitlist for this section 188 188 :return: whether the user is on the waitlist for this section
""" 189 189 """
return self.whitelist.filter(email=user.email).exists() 190 190 return self.whitelist.filter(email=user.email).exists()
191 191
class Meta: 192 192 class Meta:
ordering = ['-course_title'] 193 193 ordering = ['-course_title']
194 194
@property 195 195 @property
def lecture_times(self): 196 196 def lecture_times(self):
return ', '.join(map(lambda x: '%s %s' % (x.weekday_letter, x.short_start_time), self.lectureperiod_set.all())) 197 197 return ', '.join(map(lambda x: '%s %s' % (x.weekday_letter, x.short_start_time), self.lectureperiod_set.all()))
198 198
@property 199 199 @property
def long_name(self): 200 200 def long_name(self):
return '%s (%s) (%s)' % (self.course_title, self.instructor, self.lecture_times) 201 201 return '%s (%s) (%s)' % (self.course_title, self.instructor, self.lecture_times)
202 202
@property 203 203 @property
def short_name(self): 204 204 def short_name(self):
return '%s %s' % (self.department_abbreviation, self.course_num) 205 205 return '%s %s' % (self.department_abbreviation, self.course_num)
206 206
def __unicode__(self): 207 207 def __unicode__(self):
return '%s %s: %s (%s %s)' % ( 208 208 return '%s %s: %s (%s %s)' % (
self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) 209 209 self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter)
flashcards/views.py View file @ 6158afb
from django.contrib import auth 1 1 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 3 3 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 5 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 6 6 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 7 7 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 8 8 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 9 9 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 10 10 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 11 11 from django.core.mail import send_mail
from django.contrib.auth import authenticate 12 12 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 13 13 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 14 14 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 15 15 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 16 16 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 17 17 from simple_email_confirmation import EmailAddress
18 18
19 19
class SectionViewSet(ReadOnlyModelViewSet): 20 20 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 21 21 queryset = Section.objects.all()
serializer_class = SectionSerializer 22 22 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 23 23 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 24 24 permission_classes = [IsAuthenticated]
25 25
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 26 26 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 27 27 def flashcards(self, request, pk):
""" 28 28 """
Gets flashcards for a section, excluding hidden cards. 29 29 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 30 30 Returned in strictly chronological order (material date).
""" 31 31 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 32 32 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object(), is_hidden=False).all() 33 33 section=self.get_object(), is_hidden=False).all()
34 34
return Response(FlashcardSerializer(flashcards, many=True).data) 35 35 return Response(FlashcardSerializer(flashcards, many=True).data)
36 36
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 37 37 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 38 38 def enroll(self, request, pk):
""" 39 39 """
Add the current user to a specified section 40 40 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. 41 41 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 42 42 ---
omit_serializer: true 43 43 omit_serializer: true
parameters: 44 44 parameters:
- fake: None 45 45 - fake: None
parameters_strategy: 46 46 parameters_strategy:
form: replace 47 47 form: replace
""" 48 48 """
section = self.get_object() 49 49 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 50 50 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 51 51 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 52 52 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.") 53 53 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 54 54 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 55 55 return Response(status=HTTP_204_NO_CONTENT)
56 56
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 57 57 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 58 58 def drop(self, request, pk):
""" 59 59 """
Remove the current user from a specified section 60 60 Remove the current user from a specified section
If the user is not in the class, the request will fail. 61 61 If the user is not in the class, the request will fail.
--- 62 62 ---
omit_serializer: true 63 63 omit_serializer: true
parameters: 64 64 parameters:
- fake: None 65 65 - fake: None
parameters_strategy: 66 66 parameters_strategy:
form: replace 67 67 form: replace
""" 68 68 """
section = self.get_object() 69 69 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 70 70 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 71 71 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 72 72 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 73 73 return Response(status=HTTP_204_NO_CONTENT)
74 74
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 75 75 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 76 76 def search(self, request):
query = request.GET.get('q', None) 77 77 query = request.GET.get('q', None)
if not query: return Response('[]') 78 78 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:8] 79 79 qs = Section.search(query.split(' '))[:8]
serializer = SectionSerializer(qs, many=True) 80 80 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 81 81 return Response(serializer.data)
82 82
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 83 83 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def deck(self, request): 84 84 def deck(self, request, pk):
""" 85 85 """
Gets the contents of a user's deck for a given section. 86 86 Gets the contents of a user's deck for a given section.
""" 87 87 """
qs = Flashcard.objects.all() 88 88 qs = Flashcard.objects.all()
qs = qs.filter(userflashcard__user=request.user) 89 89 qs = qs.filter(userflashcard__user=request.user)
90 qs = qs.filter(section = self.get_object())
serializer = FlashcardSerializer(qs, many=True) 90 91 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 91 92 return Response(serializer.data)
92 93
93 94
class UserSectionListView(ListAPIView): 94 95 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 95 96 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 96 97 permission_classes = [IsAuthenticated]
97 98
def get_queryset(self): 98 99 def get_queryset(self):
return self.request.user.sections.all() 99 100 return self.request.user.sections.all()
100 101
def paginate_queryset(self, queryset): return None 101 102 def paginate_queryset(self, queryset): return None
102 103
103 104
class UserDetail(GenericAPIView): 104 105 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 105 106 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 106 107 permission_classes = [IsAuthenticated]
107 108
def get_queryset(self): 108 109 def get_queryset(self):
return User.objects.all() 109 110 return User.objects.all()
110 111
def patch(self, request, format=None): 111 112 def patch(self, request, format=None):
""" 112 113 """
Updates the user's password, or verifies their email address 113 114 Updates the user's password, or verifies their email address
--- 114 115 ---
request_serializer: UserUpdateSerializer 115 116 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 116 117 response_serializer: UserSerializer
""" 117 118 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 118 119 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 119 120 data.is_valid(raise_exception=True)
data = data.validated_data 120 121 data = data.validated_data
121 122
if 'new_password' in data: 122 123 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 123 124 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 124 125 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 125 126 request.user.set_password(data['new_password'])
request.user.save() 126 127 request.user.save()
127 128
if 'confirmation_key' in data: 128 129 if 'confirmation_key' in data:
try: 129 130 try:
request.user.confirm_email(data['confirmation_key']) 130 131 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 131 132 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 132 133 raise ValidationError('confirmation_key is invalid')
133 134
return Response(UserSerializer(request.user).data) 134 135 return Response(UserSerializer(request.user).data)
135 136
def get(self, request, format=None): 136 137 def get(self, request, format=None):
""" 137 138 """
Return data about the user 138 139 Return data about the user
--- 139 140 ---
response_serializer: UserSerializer 140 141 response_serializer: UserSerializer
""" 141 142 """
serializer = UserSerializer(request.user, context={'request': request}) 142 143 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 143 144 return Response(serializer.data)
144 145
def delete(self, request): 145 146 def delete(self, request):
""" 146 147 """
Irrevocably delete the user and their data 147 148 Irrevocably delete the user and their data
148 149
Yes, really 149 150 Yes, really
""" 150 151 """
request.user.delete() 151 152 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 152 153 return Response(status=HTTP_204_NO_CONTENT)
153 154
154 155
@api_view(['POST']) 155 156 @api_view(['POST'])
def register(request, format=None): 156 157 def register(request, format=None):
""" 157 158 """
Register a new user 158 159 Register a new user
--- 159 160 ---
request_serializer: EmailPasswordSerializer 160 161 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 161 162 response_serializer: UserSerializer
""" 162 163 """
data = RegistrationSerializer(data=request.data) 163 164 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 164 165 data.is_valid(raise_exception=True)
165 166
User.objects.create_user(**data.validated_data) 166 167 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 167 168 user = authenticate(**data.validated_data)
auth.login(request, user) 168 169 auth.login(request, user)
169 170
body = ''' 170 171 body = '''
Visit the following link to confirm your email address: 171 172 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 172 173 https://flashy.cards/app/verify_email/%s
173 174
If you did not register for Flashy, no action is required. 174 175 If you did not register for Flashy, no action is required.
''' 175 176 '''
176 177
assert send_mail("Flashy email verification", 177 178 assert send_mail("Flashy email verification",
body % user.confirmation_key, 178 179 body % user.confirmation_key,
"noreply@flashy.cards", 179 180 "noreply@flashy.cards",
[user.email]) 180 181 [user.email])
181 182
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 182 183 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
183 184
184 185
@api_view(['POST']) 185 186 @api_view(['POST'])
def login(request): 186 187 def login(request):
""" 187 188 """
Authenticates user and returns user data if valid. 188 189 Authenticates user and returns user data if valid.
--- 189 190 ---
request_serializer: EmailPasswordSerializer 190 191 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 191 192 response_serializer: UserSerializer
""" 192 193 """
193 194
data = EmailPasswordSerializer(data=request.data) 194 195 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 195 196 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 196 197 user = authenticate(**data.validated_data)
197 198
if user is None: 198 199 if user is None:
raise AuthenticationFailed('Invalid email or password') 199 200 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 200 201 if not user.is_active:
raise NotAuthenticated('Account is disabled') 201 202 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 202 203 auth.login(request, user)
return Response(UserSerializer(request.user).data) 203 204 return Response(UserSerializer(request.user).data)
204 205
205 206
@api_view(['POST']) 206 207 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 207 208 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 208 209 def logout(request, format=None):
""" 209 210 """
Logs the authenticated user out. 210 211 Logs the authenticated user out.
""" 211 212 """
auth.logout(request) 212 213 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 213 214 return Response(status=HTTP_204_NO_CONTENT)
214 215
215 216
@api_view(['POST']) 216 217 @api_view(['POST'])
def request_password_reset(request, format=None): 217 218 def request_password_reset(request, format=None):
""" 218 219 """
Send a password reset token/link to the provided email. 219 220 Send a password reset token/link to the provided email.
--- 220 221 ---
request_serializer: PasswordResetRequestSerializer 221 222 request_serializer: PasswordResetRequestSerializer
""" 222 223 """
data = PasswordResetRequestSerializer(data=request.data) 223 224 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 224 225 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 225 226 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 226 227 token = default_token_generator.make_token(user)
227 228
body = ''' 228 229 body = '''
Visit the following link to reset your password: 229 230 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 230 231 https://flashy.cards/app/reset_password/%d/%s
231 232
If you did not request a password reset, no action is required. 232 233 If you did not request a password reset, no action is required.
''' 233 234 '''
234 235
send_mail("Flashy password reset", 235 236 send_mail("Flashy password reset",
body % (user.pk, token), 236 237 body % (user.pk, token),
"noreply@flashy.cards", 237 238 "noreply@flashy.cards",
[user.email]) 238 239 [user.email])
239 240
return Response(status=HTTP_204_NO_CONTENT) 240 241 return Response(status=HTTP_204_NO_CONTENT)
241 242
242 243
@api_view(['POST']) 243 244 @api_view(['POST'])
def reset_password(request, format=None): 244 245 def reset_password(request, format=None):
""" 245 246 """
Updates user's password to new password if token is valid. 246 247 Updates user's password to new password if token is valid.
--- 247 248 ---
request_serializer: PasswordResetSerializer 248 249 request_serializer: PasswordResetSerializer
""" 249 250 """
data = PasswordResetSerializer(data=request.data) 250 251 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 251 252 data.is_valid(raise_exception=True)
252 253
user = User.objects.get(id=data['uid'].value) 253 254 user = User.objects.get(id=data['uid'].value)
# Check token validity. 254 255 # Check token validity.
255 256
if default_token_generator.check_token(user, data['token'].value): 256 257 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 257 258 user.set_password(data['new_password'].value)