Commit 72250295c643eca65164df6a55c632dedc04f078

Authored by Chung Wang
Exists in master

Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend

Showing 3 changed files Inline Diff

Flashy requires Python 2. Srsly 1 1 Flashy requires Python 2. Srsly
2 2
All of these commands should be run from this directory (the one containing README.md) 3 3 All of these commands should be run from this directory (the one containing README.md)
4 4
5 Virtualenv for Windows creates a dir inexplicably named scripts rather than bin. So substitute venv/bin for venv/scripts if you are on Windows.
6
Install virtualenv before continuing. This is most easily accomplished with: 5 7 Install virtualenv before continuing. This is most easily accomplished with:
6 8
pip install virtualenv 7 9 pip install virtualenv
8 10
Set up the environment by running: 9 11 Set up the environment by running:
10 12
scripts/setup.sh 11 13 scripts/setup.sh
12 14
If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv: 13 15 If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv:
14 16
. venv/bin/activate 15 17 . venv/bin/activate
16 18
flashcards/models.py View file @ 7225029
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied 2 2 from django.core.exceptions import PermissionDenied
from django.db.models import * 3 3 from django.db.models import *
from django.utils.timezone import now 4 4 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 5 5 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 6 6 from fields import MaskField
from datetime import datetime 7 7 from datetime import datetime
8 8
# Hack to fix AbstractUser before subclassing it 9 9 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 10 10 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 11 11 AbstractUser._meta.get_field('username')._unique = False
12 12
13 13
class EmailOnlyUserManager(UserManager): 14 14 class EmailOnlyUserManager(UserManager):
""" 15 15 """
A tiny extension of Django's UserManager which correctly creates users 16 16 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 17 17 without usernames (using emails instead).
""" 18 18 """
19 19
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 20 20 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 21 21 """
Creates and saves a User with the given email and password. 22 22 Creates and saves a User with the given email and password.
""" 23 23 """
email = self.normalize_email(email) 24 24 email = self.normalize_email(email)
user = self.model(email=email, 25 25 user = self.model(email=email,
is_staff=is_staff, is_active=True, 26 26 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 27 27 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 28 28 date_joined=now(), **extra_fields)
user.set_password(password) 29 29 user.set_password(password)
user.save(using=self._db) 30 30 user.save(using=self._db)
return user 31 31 return user
32 32
def create_user(self, email, password=None, **extra_fields): 33 33 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 34 34 return self._create_user(email, password, False, False, **extra_fields)
35 35
def create_superuser(self, email, password, **extra_fields): 36 36 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 37 37 return self._create_user(email, password, True, True, **extra_fields)
38 38
39 39
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 40 40 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 41 41 """
An extension of Django's default user model. 42 42 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 43 43 We use email as the username field, and include enrolled sections here
""" 44 44 """
objects = EmailOnlyUserManager() 45 45 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 46 46 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 47 47 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 48 48 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
49 49
def is_in_section(self, section): 50 50 def is_in_section(self, section):
return section in self.sections.all() 51 51 return section in self.sections.all()
52 52
def pull(self, flashcard): 53 53 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 54 54 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 55 55 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 56 56 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 57 57 user_card.pulled = datetime.now()
user_card.save() 58 58 user_card.save()
59 59
60 60
class UserFlashcard(Model): 61 61 class UserFlashcard(Model):
""" 62 62 """
Represents the relationship between a user and a flashcard by: 63 63 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 64 64 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 65 65 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 66 66 3. A user has a flashcard hidden from them
""" 67 67 """
user = ForeignKey('User') 68 68 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 69 69 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 70 70 help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") 71 71 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 72 72 flashcard = ForeignKey('Flashcard')
73 73
class Meta: 74 74 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 75 75 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 76 76 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 77 77 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 78 78 # By default, order by most recently pulled
ordering = ['-pulled'] 79 79 ordering = ['-pulled']
80 80
81 81
class FlashcardHide(Model): 82 82 class FlashcardHide(Model):
""" 83 83 """
Represents the property of a flashcard being hidden by a user. 84 84 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 85 85 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 86 86 If reason is null, the flashcard was just hidden.
If reason is not null, the flashcard was reported, and reason is the reason why it was reported. 87 87 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 88 88 """
user = ForeignKey('User') 89 89 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 90 90 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 91 91 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 92 92 hidden = DateTimeField(auto_now_add=True)
93 93
class Meta: 94 94 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 95 95 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 96 96 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 97 97 index_together = ["user", "flashcard"]
98 98
99 99
class Flashcard(Model): 100 100 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 101 101 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') 102 102 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") 103 103 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") 104 104 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 105 105 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 106 106 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 107 107 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 108 108 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 109 109 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") 110 110 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
111 111
class Meta: 112 112 class Meta:
# By default, order by most recently pushed 113 113 # By default, order by most recently pushed
ordering = ['-pushed'] 114 114 ordering = ['-pushed']
115 115
def is_hidden_from(self, user): 116 116 def is_hidden_from(self, user):
""" 117 117 """
A card can be hidden globally, but if a user has the card in their deck, 118 118 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 119 119 this visibility overrides a global hide.
:param user: 120 120 :param user:
:return: Whether the card is hidden from the user. 121 121 :return: Whether the card is hidden from the user.
""" 122 122 """
result = user.userflashcard_set.filter(flashcard=self) 123 123 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 124 124 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 125 125 return result[0].is_hidden()
126 126
def edit(self, user, new_flashcard): 127 127 def edit(self, user, new_flashcard):
""" 128 128 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 129 129 Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard.
Sets up everything correctly so this object, when saved, will result in the appropriate changes. 130 130 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 131 131 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 132 132 :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 133 133 """
if not user.is_in_section(self.section): 134 134 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to edit this card") 135 135 raise PermissionDenied("You don't have the permission to edit this card")
136 136
# content_changed is True iff either material_date or text were changed 137 137 # content_changed is True iff either material_date or text were changed
content_changed = False 138 138 content_changed = False
# create_new is True iff the user editing this card is the author of this card 139 139 # create_new is True iff the user editing this card is the author of this card
# and there are no other users with this card in their decks 140 140 # and there are no other users with this card in their decks
create_new = user != self.author or \ 141 141 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 142 142 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
143 143
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 144 144 if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
content_changed |= True 145 145 content_changed |= True
self.material_date = new_flashcard['material_date'] 146 146 self.material_date = new_flashcard['material_date']
if 'text' in new_flashcard and self.text != new_flashcard['text']: 147 147 if 'text' in new_flashcard and self.text != new_flashcard['text']:
content_changed |= True 148 148 content_changed |= True
self.text = new_flashcard['text'] 149 149 self.text = new_flashcard['text']
if create_new and content_changed: 150 150 if create_new and content_changed:
self.pk = None 151 151 self.pk = None
if 'mask' in new_flashcard: 152 152 if 'mask' in new_flashcard:
self.mask = new_flashcard['mask'] 153 153 self.mask = new_flashcard['mask']
154 154
@classmethod 155 155 @classmethod
def cards_visible_to(cls, user): 156 156 def cards_visible_to(cls, user):
""" 157 157 """
:param user: 158 158 :param user:
:return: A queryset with all cards that should be visible to a user. 159 159 :return: A queryset with all cards that should be visible to a user.
""" 160 160 """
return cls.objects.filter(is_hidden=False).exclude(userflashcard__user=user, userflashcard__pulled=None) 161 161 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
162 162
163 163
class UserFlashcardQuiz(Model): 164 164 class UserFlashcardQuiz(Model):
""" 165 165 """
An event of a user being quizzed on a flashcard. 166 166 An event of a user being quizzed on a flashcard.
""" 167 167 """
user_flashcard = ForeignKey(UserFlashcard) 168 168 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 169 169 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 170 170 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") 171 171 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") 172 172 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
173 173
def status(self): 174 174 def status(self):
""" 175 175 """
There are three stages of a quiz object: 176 176 There are three stages of a quiz object:
1. the user has been shown the card 177 177 1. the user has been shown the card
2. the user has answered the card 178 178 2. the user has answered the card
3. the user has self-evaluated their response's correctness 179 179 3. the user has self-evaluated their response's correctness
180 180
:return: string (evaluated, answered, viewed) 181 181 :return: string (evaluated, answered, viewed)
""" 182 182 """
if self.correct is not None: return "evaluated" 183 183 if self.correct is not None: return "evaluated"
if self.response: return "answered" 184 184 if self.response: return "answered"
return "viewed" 185 185 return "viewed"
186 186
187 187
class Section(Model): 188 188 class Section(Model):
""" 189 189 """
A UCSD course taught by an instructor during a quarter. 190 190 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 191 191 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 192 192 We index gratuitously to support autofill and because this is primarily read-only
""" 193 193 """
department = CharField(db_index=True, max_length=50) 194 194 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 195 195 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 196 196 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 197 197 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 198 198 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 199 199 quarter = CharField(db_index=True, max_length=4)
200 200
@classmethod 201 201 @classmethod
def search(cls, terms): 202 202 def search(cls, terms):
""" 203 203 """
Search all fields of all sections for a particular set of terms 204 204 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 205 205 A matching section must match at least one field on each term
:param terms:iterable 206 206 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 207 207 :return: Matching QuerySet ordered by department and course number
""" 208 208 """
final_q = Q() 209 209 final_q = Q()
for term in terms: 210 210 for term in terms:
q = Q(department__icontains=term) 211 211 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 212 212 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 213 213 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 214 214 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 215 215 q |= Q(instructor__icontains=term)
final_q &= q 216 216 final_q &= q
qs = cls.objects.filter(final_q) 217 217 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 218 218 # 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 219 219 # 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)"}) 220 220 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 221 221 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 222 222 return qs
223 223
@property 224 224 @property
def is_whitelisted(self): 225 225 def is_whitelisted(self):
""" 226 226 """
:return: whether a whitelist exists for this section 227 227 :return: whether a whitelist exists for this section
""" 228 228 """
return self.whitelist.exists() 229 229 return self.whitelist.exists()
230 230
def is_user_on_whitelist(self, user): 231 231 def is_user_on_whitelist(self, user):
""" 232 232 """
:return: whether the user is on the waitlist for this section 233 233 :return: whether the user is on the waitlist for this section
""" 234 234 """
return self.whitelist.filter(email=user.email).exists() 235 235 return self.whitelist.filter(email=user.email).exists()
236 236
class Meta: 237 237 class Meta:
ordering = ['-course_title'] 238 238 ordering = ['-course_title']
239 239
@property 240 240 @property
def lecture_times(self): 241 241 def lecture_times(self):
lecture_periods = self.lectureperiod_set.all() 242 242 lecture_periods = self.lectureperiod_set.all()
flashcards/views.py View file @ 7225029
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, FlashcardHide, UserFlashcard 3 3 from flashcards.models import Section, User, Flashcard, FlashcardHide, 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, \
FlashcardUpdateSerializer 6 6 FlashcardUpdateSerializer
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
from datetime import datetime 19
20 19
21 20
class SectionViewSet(ReadOnlyModelViewSet): 22 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 23 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 24 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 25 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 26 25 permission_classes = [IsAuthenticated]
27 26
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 28 27 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 29 28 def flashcards(self, request, pk):
""" 30 29 """
Gets flashcards for a section, excluding hidden cards. 31 30 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 32 31 Returned in strictly chronological order (material date).
""" 33 32 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 34 33 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object())
section=self.get_object(), is_hidden=False).all() 35
36
return Response(FlashcardSerializer(flashcards, many=True).data) 37 34 return Response(FlashcardSerializer(flashcards, many=True).data)
38 35
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 39 36 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 40 37 def enroll(self, request, pk):
""" 41 38 """
Add the current user to a specified section 42 39 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. 43 40 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 44 41 ---
omit_serializer: true 45 42 omit_serializer: true
parameters: 46 43 parameters:
- fake: None 47 44 - fake: None
parameters_strategy: 48 45 parameters_strategy:
form: replace 49 46 form: replace
""" 50 47 """
section = self.get_object() 51 48 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 52 49 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 53 50 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 54 51 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.") 55 52 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 56 53 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 57 54 return Response(status=HTTP_204_NO_CONTENT)
58 55
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 59 56 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 60 57 def drop(self, request, pk):
""" 61 58 """
Remove the current user from a specified section 62 59 Remove the current user from a specified section
If the user is not in the class, the request will fail. 63 60 If the user is not in the class, the request will fail.
--- 64 61 ---
omit_serializer: true 65 62 omit_serializer: true
parameters: 66 63 parameters:
- fake: None 67 64 - fake: None
parameters_strategy: 68 65 parameters_strategy:
form: replace 69 66 form: replace
""" 70 67 """
section = self.get_object() 71 68 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 72 69 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 73 70 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 74 71 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 75 72 return Response(status=HTTP_204_NO_CONTENT)
76 73
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 77 74 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 78 75 def search(self, request):
query = request.GET.get('q', None) 79 76 query = request.GET.get('q', None)
if not query: return Response('[]') 80 77 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 81 78 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 82 79 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 83 80 return Response(serializer.data)
84 81
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 85 82 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def deck(self, request, pk): 86 83 def deck(self, request, pk):
""" 87 84 """
Gets the contents of a user's deck for a given section. 88 85 Gets the contents of a user's deck for a given section.
""" 89 86 """
qs = Flashcard.objects.all() 90 87 qs = Flashcard.objects.all()
qs = qs.filter(userflashcard__user=request.user) 91 88 qs = qs.filter(userflashcard__user=request.user)
qs = qs.filter(section = self.get_object()) 92 89 qs = qs.filter(section=self.get_object())
serializer = FlashcardSerializer(qs, many=True) 93 90 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 94 91 return Response(serializer.data)
95 92
#attempt to get flashcard 96 93 #attempt to get flashcard
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 97 94 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 98 95 def ordered_deck(self, request, pk):
""" 99 96 """
Get a chronological order by material_date of flashcards for a section. 100 97 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 101 98 This excludes hidden card.
""" 102 99 """
qs = Flashcard.objects.all() 103 100 qs = Flashcard.objects.all()
qs = qs.filter(userflashcard__user=request.user) 104 101 qs = qs.filter(userflashcard__user=request.user)
qs = qs.filter(section = self.get_object()) 105 102 qs = qs.filter(section = self.get_object())
qs = qs.exclude(flashcardhide__user=request.user) 106 103 qs = qs.exclude(flashcardhide__user=request.user)
qs = qs.order_by('-material_date') 107 104 qs = qs.order_by('-material_date')
108 105
serializer = FlashcardSerializer(qs, many=True) 109 106 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 110 107 return Response(serializer.data)
111 108
112 109
class UserSectionListView(ListAPIView): 113 110 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 114 111 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 115 112 permission_classes = [IsAuthenticated]
116 113
def get_queryset(self): 117 114 def get_queryset(self):
return self.request.user.sections.all() 118 115 return self.request.user.sections.all()
119 116
def paginate_queryset(self, queryset): return None 120 117 def paginate_queryset(self, queryset): return None
121 118
122 119
class UserDetail(GenericAPIView): 123 120 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 124 121 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 125 122 permission_classes = [IsAuthenticated]
126 123
def get_queryset(self): 127 124 def get_queryset(self):
return User.objects.all() 128 125 return User.objects.all()
129 126
def patch(self, request, format=None): 130 127 def patch(self, request, format=None):
""" 131 128 """
Updates the user's password, or verifies their email address 132 129 Updates the user's password, or verifies their email address
--- 133 130 ---
request_serializer: UserUpdateSerializer 134 131 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 135 132 response_serializer: UserSerializer
""" 136 133 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 137 134 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 138 135 data.is_valid(raise_exception=True)
data = data.validated_data 139 136 data = data.validated_data
140 137
if 'new_password' in data: 141 138 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 142 139 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 143 140 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 144 141 request.user.set_password(data['new_password'])
request.user.save() 145 142 request.user.save()
146 143
if 'confirmation_key' in data: 147 144 if 'confirmation_key' in data:
try: 148 145 try:
request.user.confirm_email(data['confirmation_key']) 149 146 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 150 147 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 151 148 raise ValidationError('confirmation_key is invalid')
152 149
return Response(UserSerializer(request.user).data) 153 150 return Response(UserSerializer(request.user).data)
154 151
def get(self, request, format=None): 155 152 def get(self, request, format=None):
""" 156 153 """
Return data about the user 157 154 Return data about the user
--- 158 155 ---
response_serializer: UserSerializer 159 156 response_serializer: UserSerializer
""" 160 157 """
serializer = UserSerializer(request.user, context={'request': request}) 161 158 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 162 159 return Response(serializer.data)
163 160
def delete(self, request): 164 161 def delete(self, request):
""" 165 162 """
Irrevocably delete the user and their data 166 163 Irrevocably delete the user and their data
167 164
Yes, really 168 165 Yes, really
""" 169 166 """
request.user.delete() 170 167 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 171 168 return Response(status=HTTP_204_NO_CONTENT)
172 169
173 170
@api_view(['POST']) 174 171 @api_view(['POST'])
def register(request, format=None): 175 172 def register(request, format=None):
""" 176 173 """
Register a new user 177 174 Register a new user
--- 178 175 ---
request_serializer: EmailPasswordSerializer 179 176 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 180 177 response_serializer: UserSerializer
""" 181 178 """
data = RegistrationSerializer(data=request.data) 182 179 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 183 180 data.is_valid(raise_exception=True)
184 181
User.objects.create_user(**data.validated_data) 185 182 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 186 183 user = authenticate(**data.validated_data)
auth.login(request, user) 187 184 auth.login(request, user)
188 185
body = ''' 189 186 body = '''
Visit the following link to confirm your email address: 190 187 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 191 188 https://flashy.cards/app/verify_email/%s
192 189
If you did not register for Flashy, no action is required. 193 190 If you did not register for Flashy, no action is required.
''' 194 191 '''
195 192
assert send_mail("Flashy email verification", 196 193 assert send_mail("Flashy email verification",
body % user.confirmation_key, 197 194 body % user.confirmation_key,
"noreply@flashy.cards", 198 195 "noreply@flashy.cards",
[user.email]) 199 196 [user.email])
200 197
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 201 198 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
202 199
203 200
@api_view(['POST']) 204 201 @api_view(['POST'])
def login(request): 205 202 def login(request):
""" 206 203 """
Authenticates user and returns user data if valid. 207 204 Authenticates user and returns user data if valid.
--- 208 205 ---
request_serializer: EmailPasswordSerializer 209 206 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 210 207 response_serializer: UserSerializer
""" 211 208 """
212 209
data = EmailPasswordSerializer(data=request.data) 213 210 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 214 211 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 215 212 user = authenticate(**data.validated_data)
216 213
if user is None: 217 214 if user is None:
raise AuthenticationFailed('Invalid email or password') 218 215 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 219 216 if not user.is_active:
raise NotAuthenticated('Account is disabled') 220 217 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 221 218 auth.login(request, user)
return Response(UserSerializer(request.user).data) 222 219 return Response(UserSerializer(request.user).data)
223 220
224 221
@api_view(['POST']) 225 222 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 226 223 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 227 224 def logout(request, format=None):
""" 228 225 """
Logs the authenticated user out. 229 226 Logs the authenticated user out.
""" 230 227 """
auth.logout(request) 231 228 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 232 229 return Response(status=HTTP_204_NO_CONTENT)
233 230
234 231
@api_view(['POST']) 235 232 @api_view(['POST'])
def request_password_reset(request, format=None): 236 233 def request_password_reset(request, format=None):
""" 237 234 """
Send a password reset token/link to the provided email. 238 235 Send a password reset token/link to the provided email.
--- 239 236 ---
request_serializer: PasswordResetRequestSerializer 240 237 request_serializer: PasswordResetRequestSerializer
""" 241 238 """
data = PasswordResetRequestSerializer(data=request.data) 242 239 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 243 240 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 244 241 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 245 242 token = default_token_generator.make_token(user)
246 243
body = ''' 247 244 body = '''
Visit the following link to reset your password: 248 245 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 249 246 https://flashy.cards/app/reset_password/%d/%s
250 247
If you did not request a password reset, no action is required. 251 248 If you did not request a password reset, no action is required.
''' 252 249 '''
253 250
send_mail("Flashy password reset", 254 251 send_mail("Flashy password reset",
body % (user.pk, token), 255 252 body % (user.pk, token),
"noreply@flashy.cards", 256 253 "noreply@flashy.cards",
[user.email]) 257 254 [user.email])
258 255
return Response(status=HTTP_204_NO_CONTENT) 259 256 return Response(status=HTTP_204_NO_CONTENT)
260 257
261 258
@api_view(['POST']) 262 259 @api_view(['POST'])
def reset_password(request, format=None): 263 260 def reset_password(request, format=None):
""" 264 261 """
Updates user's password to new password if token is valid. 265 262 Updates user's password to new password if token is valid.
--- 266 263 ---
request_serializer: PasswordResetSerializer 267 264 request_serializer: PasswordResetSerializer
""" 268 265 """
data = PasswordResetSerializer(data=request.data) 269 266 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 270 267 data.is_valid(raise_exception=True)
271 268
user = User.objects.get(id=data['uid'].value) 272 269 user = User.objects.get(id=data['uid'].value)
# Check token validity. 273 270 # Check token validity.
274 271
if default_token_generator.check_token(user, data['token'].value): 275 272 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 276 273 user.set_password(data['new_password'].value)
user.save() 277 274 user.save()
else: 278 275 else:
raise ValidationError('Could not verify reset token') 279 276 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 280 277 return Response(status=HTTP_204_NO_CONTENT)
281 278
282 279
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 283 280 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 284 281 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 285 282 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 286 283 permission_classes = [IsAuthenticated]
287 284
# Override create in CreateModelMixin 288 285 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 289 286 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 290 287 serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) 291 288 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 292 289 serializer.validated_data['author'] = request.user
self.perform_create(serializer) 293 290 self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) 294 291 headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 295 292 return Response(serializer.data, status=HTTP_201_CREATED, headers=headers)
296 293
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 297 294 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])