Commit 5d861cbfb8e4b0316de8ccec0c804809ed7090f3

Authored by Rohan Rangray
1 parent 61c43267c4
Exists in master

Wrote tests for FlashcardViewSet.edit

Showing 4 changed files with 77 additions and 20 deletions Inline Diff

flashcards/models.py View file @ 5d861cb
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.contrib.auth.models import AbstractUser, UserManager 3 3 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied 4 4 from django.core.exceptions import PermissionDenied, SuspiciousOperation
5 from django.db import IntegrityError
from django.db.models import * 5 6 from django.db.models import *
from django.utils.timezone import now 6 7 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 7 8 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 8 9 from fields import MaskField
9 10
10 11
# Hack to fix AbstractUser before subclassing it 11 12 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 12 13 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 13 14 AbstractUser._meta.get_field('username')._unique = False
14 15
15 16
class EmailOnlyUserManager(UserManager): 16 17 class EmailOnlyUserManager(UserManager):
""" 17 18 """
A tiny extension of Django's UserManager which correctly creates users 18 19 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 19 20 without usernames (using emails instead).
""" 20 21 """
21 22
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 22 23 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 23 24 """
Creates and saves a User with the given email and password. 24 25 Creates and saves a User with the given email and password.
""" 25 26 """
email = self.normalize_email(email) 26 27 email = self.normalize_email(email)
user = self.model(email=email, 27 28 user = self.model(email=email,
is_staff=is_staff, is_active=True, 28 29 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 29 30 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 30 31 date_joined=now(), **extra_fields)
user.set_password(password) 31 32 user.set_password(password)
user.save(using=self._db) 32 33 user.save(using=self._db)
return user 33 34 return user
34 35
def create_user(self, email, password=None, **extra_fields): 35 36 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 36 37 return self._create_user(email, password, False, False, **extra_fields)
37 38
def create_superuser(self, email, password, **extra_fields): 38 39 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 39 40 return self._create_user(email, password, True, True, **extra_fields)
40 41
41 42
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 42 43 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 43 44 """
An extension of Django's default user model. 44 45 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 45 46 We use email as the username field, and include enrolled sections here
""" 46 47 """
objects = EmailOnlyUserManager() 47 48 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 48 49 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 49 50 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 50 51 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
51 52
def is_in_section(self, section): 52 53 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 53 54 return self.sections.filter(pk=section.pk).exists()
54 55
def pull(self, flashcard): 55 56 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 56 57 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 57 58 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 58 59 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 59 60 user_card.pulled = datetime.now()
user_card.save() 60 61 user_card.save()
61 62
def get_deck(self, section): 62 63 def get_deck(self, section):
if not self.is_in_section(section): 63 64 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 64 65 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 65 66 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
66 67
67 68
class UserFlashcard(Model): 68 69 class UserFlashcard(Model):
""" 69 70 """
Represents the relationship between a user and a flashcard by: 70 71 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 71 72 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 72 73 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 73 74 3. A user has a flashcard hidden from them
""" 74 75 """
user = ForeignKey('User') 75 76 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 76 77 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 77 78 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") 78 79 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 79 80 flashcard = ForeignKey('Flashcard')
80 81
class Meta: 81 82 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 82 83 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 83 84 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 84 85 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 85 86 # By default, order by most recently pulled
ordering = ['-pulled'] 86 87 ordering = ['-pulled']
87 88
88 89
class FlashcardHide(Model): 89 90 class FlashcardHide(Model):
""" 90 91 """
Represents the property of a flashcard being hidden by a user. 91 92 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 92 93 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 93 94 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. 94 95 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 95 96 """
user = ForeignKey('User') 96 97 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 97 98 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 98 99 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 99 100 hidden = DateTimeField(auto_now_add=True)
100 101
class Meta: 101 102 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 102 103 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 103 104 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 104 105 index_together = ["user", "flashcard"]
105 106
106 107
class Flashcard(Model): 107 108 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 108 109 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') 109 110 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") 110 111 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") 111 112 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 112 113 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 113 114 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 114 115 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 115 116 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 116 117 hide_reason = CharField(blank=True, null=True, max_length=255,
118 default=None, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 117 119 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
118 120
class Meta: 119 121 class Meta:
# By default, order by most recently pushed 120 122 # By default, order by most recently pushed
ordering = ['-pushed'] 121 123 ordering = ['-pushed']
122 124
def is_hidden_from(self, user): 123 125 def is_hidden_from(self, user):
""" 124 126 """
A card can be hidden globally, but if a user has the card in their deck, 125 127 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 126 128 this visibility overrides a global hide.
:param user: 127 129 :param user:
:return: Whether the card is hidden from the user. 128 130 :return: Whether the card is hidden from the user.
""" 129 131 """
result = user.userflashcard_set.filter(flashcard=self) 130 132 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 131 133 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 132 134 return result[0].is_hidden()
133 135
136 def add_to_deck(self, user):
137 if not user.is_in_section(self.section):
138 raise PermissionDenied("You don't have the permission to add this card")
139 try:
140 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
141 except IntegrityError:
142 raise SuspiciousOperation("The flashcard is already in the user's deck")
143 user_flashcard.save()
144 return user_flashcard
145
def edit(self, user, new_flashcard): 134 146 def edit(self, user, new_flashcard):
""" 135 147 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 136 148 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. 137 149 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 138 150 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 139 151 :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 140 152 """
if not user.is_in_section(self.section): 141 153 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to edit this card") 142 154 raise PermissionDenied("You don't have the permission to edit this card")
143 155
# content_changed is True iff either material_date or text were changed 144 156 # content_changed is True iff either material_date or text were changed
content_changed = False 145 157 content_changed = False
# create_new is True iff the user editing this card is the author of this card 146 158 # 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 147 159 # and there are no other users with this card in their decks
create_new = user != self.author or \ 148 160 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 149 161 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
150 162
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 151 163 if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
content_changed |= True 152 164 content_changed |= True
self.material_date = new_flashcard['material_date'] 153 165 self.material_date = new_flashcard['material_date']
if 'text' in new_flashcard and self.text != new_flashcard['text']: 154 166 if 'text' in new_flashcard and self.text != new_flashcard['text']:
content_changed |= True 155 167 content_changed |= True
self.text = new_flashcard['text'] 156 168 self.text = new_flashcard['text']
if create_new and content_changed: 157 169 if create_new and content_changed:
170 mask = self.mask
171 pk = self.pk
self.pk = None 158 172 self.pk = None
if 'mask' in new_flashcard: 159 173 if 'mask' in new_flashcard:
self.mask = new_flashcard['mask'] 160 174 mask = new_flashcard['mask']
175 self.mask = mask
176 self.previous_id = pk
self.save() 161 177 self.save()
178 return create_new and content_changed
162 179
@classmethod 163 180 @classmethod
def cards_visible_to(cls, user): 164 181 def cards_visible_to(cls, user):
""" 165 182 """
:param user: 166 183 :param user:
:return: A queryset with all cards that should be visible to a user. 167 184 :return: A queryset with all cards that should be visible to a user.
""" 168 185 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 169 186 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
170 187
171 188
class UserFlashcardQuiz(Model): 172 189 class UserFlashcardQuiz(Model):
""" 173 190 """
An event of a user being quizzed on a flashcard. 174 191 An event of a user being quizzed on a flashcard.
""" 175 192 """
user_flashcard = ForeignKey(UserFlashcard) 176 193 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 177 194 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 178 195 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") 179 196 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") 180 197 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
181 198
def status(self): 182 199 def status(self):
""" 183 200 """
There are three stages of a quiz object: 184 201 There are three stages of a quiz object:
1. the user has been shown the card 185 202 1. the user has been shown the card
2. the user has answered the card 186 203 2. the user has answered the card
3. the user has self-evaluated their response's correctness 187 204 3. the user has self-evaluated their response's correctness
188 205
:return: string (evaluated, answered, viewed) 189 206 :return: string (evaluated, answered, viewed)
""" 190 207 """
if self.correct is not None: return "evaluated" 191 208 if self.correct is not None: return "evaluated"
if self.response: return "answered" 192 209 if self.response: return "answered"
return "viewed" 193 210 return "viewed"
194 211
195 212
class Section(Model): 196 213 class Section(Model):
""" 197 214 """
A UCSD course taught by an instructor during a quarter. 198 215 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 199 216 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 200 217 We index gratuitously to support autofill and because this is primarily read-only
""" 201 218 """
department = CharField(db_index=True, max_length=50) 202 219 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 203 220 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 204 221 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 205 222 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 206 223 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 207 224 quarter = CharField(db_index=True, max_length=4)
208 225
@classmethod 209 226 @classmethod
def search(cls, terms): 210 227 def search(cls, terms):
""" 211 228 """
Search all fields of all sections for a particular set of terms 212 229 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 213 230 A matching section must match at least one field on each term
:param terms:iterable 214 231 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 215 232 :return: Matching QuerySet ordered by department and course number
""" 216 233 """
final_q = Q() 217 234 final_q = Q()
for term in terms: 218 235 for term in terms:
q = Q(department__icontains=term) 219 236 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 220 237 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 221 238 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 222 239 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 223 240 q |= Q(instructor__icontains=term)
final_q &= q 224 241 final_q &= q
qs = cls.objects.filter(final_q) 225 242 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 226 243 # 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 227 244 # 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)"}) 228 245 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 229 246 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 230 247 return qs
231 248
@property 232 249 @property
def is_whitelisted(self): 233 250 def is_whitelisted(self):
""" 234 251 """
:return: whether a whitelist exists for this section 235 252 :return: whether a whitelist exists for this section
""" 236 253 """
return self.whitelist.exists() 237 254 return self.whitelist.exists()
238 255
def is_user_on_whitelist(self, user): 239 256 def is_user_on_whitelist(self, user):
""" 240 257 """
:return: whether the user is on the waitlist for this section 241 258 :return: whether the user is on the waitlist for this section
""" 242 259 """
return self.whitelist.filter(email=user.email).exists() 243 260 return self.whitelist.filter(email=user.email).exists()
244 261
class Meta: 245 262 class Meta:
ordering = ['-course_title'] 246 263 ordering = ['-course_title']
247 264
@property 248 265 @property
def lecture_times(self): 249 266 def lecture_times(self):
lecture_periods = self.lectureperiod_set.all() 250 267 lecture_periods = self.lectureperiod_set.all()
if not lecture_periods.exists(): return '' 251 268 if not lecture_periods.exists(): return ''
return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time 252 269 return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time
253 270
@property 254 271 @property
flashcards/serializers.py View file @ 5d861cb
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 2 2 from django.utils.timezone import now
import pytz 3 3 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard 4 4 from flashcards.models import Section, LecturePeriod, User, Flashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 6 6 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 7 7 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
from rest_framework.relations import HyperlinkedRelatedField 8 8 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 9 9 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 10 10 from rest_framework.validators import UniqueValidator
from json import dumps, loads 11 11 from json import dumps, loads
12 12
13 13
class EmailSerializer(Serializer): 14 14 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 15 email = EmailField(required=True)
16 16
17 17
class EmailPasswordSerializer(EmailSerializer): 18 18 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 19 password = CharField(required=True)
20 20
21 21
class RegistrationSerializer(EmailPasswordSerializer): 22 22 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 23 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 24
25 25
class PasswordResetRequestSerializer(EmailSerializer): 26 26 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 27 def validate_email(self, value):
try: 28 28 try:
User.objects.get(email=value) 29 29 User.objects.get(email=value)
return value 30 30 return value
except User.DoesNotExist: 31 31 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 32 raise serializers.ValidationError('No user exists with that email')
33 33
34 34
class PasswordResetSerializer(Serializer): 35 35 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 36 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 37 uid = IntegerField(required=True)
token = CharField(required=True) 38 38 token = CharField(required=True)
39 39
def validate_uid(self, value): 40 40 def validate_uid(self, value):
try: 41 41 try:
User.objects.get(id=value) 42 42 User.objects.get(id=value)
return value 43 43 return value
except User.DoesNotExist: 44 44 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 45 raise serializers.ValidationError('Could not verify reset token')
46 46
47 47
class UserUpdateSerializer(Serializer): 48 48 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 49 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 50 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 51 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 52 # reset_token = CharField(required=False)
53 53
def validate(self, data): 54 54 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 55 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 56 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 57 return data
58 58
59 59
class Password(Serializer): 60 60 class Password(Serializer):
email = EmailField(required=True) 61 61 email = EmailField(required=True)
password = CharField(required=True) 62 62 password = CharField(required=True)
63 63
64 64
class LecturePeriodSerializer(ModelSerializer): 65 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 66 class Meta:
model = LecturePeriod 67 67 model = LecturePeriod
exclude = 'id', 'section' 68 68 exclude = 'id', 'section'
69 69
70 70
class SectionSerializer(ModelSerializer): 71 71 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 72 72 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
lecture_times = CharField() 73 73 lecture_times = CharField()
short_name = CharField() 74 74 short_name = CharField()
long_name = CharField() 75 75 long_name = CharField()
76 76
class Meta: 77 77 class Meta:
model = Section 78 78 model = Section
79 79
80 80
class UserSerializer(ModelSerializer): 81 81 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 82 82 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 83 83 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 84 84 is_confirmed = BooleanField()
85 85
class Meta: 86 86 class Meta:
model = User 87 87 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 88 88 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
89 89
90 90
class MaskFieldSerializer(serializers.Field): 91 91 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 92 92 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 93 93 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 94 94 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 95 95 'overlap': 'Ensure this field does not have overlapping intervals.'
} 96 96 }
97 97
def to_representation(self, value): 98 98 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 99 99 return dumps(list(self._make_mask(value)))
100 100
def to_internal_value(self, value): 101 101 def to_internal_value(self, value):
return self._make_mask(loads(value)) 102 102 return self._make_mask(loads(value))
103 103
def _make_mask(self, data): 104 104 def _make_mask(self, data):
try: 105 105 try:
mask = FlashcardMask(data) 106 106 mask = FlashcardMask(data)
except ValueError: 107 107 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 108 108 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 109 109 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 110 110 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 111 111 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 112 112 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 113 113 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 114 114 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 115 115 return mask
116 116
117 117
class FlashcardSerializer(ModelSerializer): 118 118 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 119 119 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 120 120 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 121 121 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer() 122 122 mask = MaskFieldSerializer(allow_null=True)
123 123
def validate_material_date(self, value): 124 124 def validate_material_date(self, value):
utc = pytz.UTC 125 125 utc = pytz.UTC
# TODO: make this dynamic 126 126 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 127 127 quarter_start = utc.localize(datetime(2015, 3, 15))
quarter_end = utc.localize(datetime(2015, 6, 15)) 128 128 quarter_end = utc.localize(datetime(2015, 6, 15))
129 129
if quarter_start <= value <= quarter_end: 130 130 if quarter_start <= value <= quarter_end:
return value 131 131 return value
else: 132 132 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 133 133 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
134 134
def validate_previous(self, value): 135 135 def validate_previous(self, value):
if value is None: 136 136 if value is None:
return value 137 137 return value
if Flashcard.objects.filter(pk=value).count() > 0: 138 138 if Flashcard.objects.filter(pk=value.pk).count() > 0:
return value 139 139 return value
raise serializers.ValidationError("Invalid previous Flashcard object") 140 140 raise serializers.ValidationError("Invalid previous Flashcard object")
141 141
def validate_pushed(self, value): 142 142 def validate_pushed(self, value):
if value > datetime.now(): 143 143 if value > datetime.now():
flashcards/tests/test_api.py View file @ 5d861cb
from django.core import mail 1 1 from django.core import mail
from flashcards.models import * 2 2 from flashcards.models import *
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN, HTTP_404_NOT_FOUND
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
from django.utils.timezone import now 6 6 from django.utils.timezone import now
7 from flashcards.validators import FlashcardMask
8 from flashcards.serializers import FlashcardSerializer
9 from pytz import UTC as utc
7 10
8 11
class LoginTests(APITestCase): 9 12 class LoginTests(APITestCase):
fixtures = ['testusers'] 10 13 fixtures = ['testusers']
11 14
def test_login(self): 12 15 def test_login(self):
url = '/api/login' 13 16 url = '/api/login'
data = {'email': 'none@none.com', 'password': '1234'} 14 17 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 15 18 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 16 19 self.assertEqual(response.status_code, HTTP_200_OK)
17 20
data = {'email': 'none@none.com', 'password': '4321'} 18 21 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 19 22 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 20 23 self.assertContains(response, 'Invalid email or password', status_code=403)
21 24
data = {'email': 'bad@none.com', 'password': '1234'} 22 25 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 23 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 24 27 self.assertContains(response, 'Invalid email or password', status_code=403)
25 28
data = {'password': '4321'} 26 29 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 27 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 28 31 self.assertContains(response, 'email', status_code=400)
29 32
data = {'email': 'none@none.com'} 30 33 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 31 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 32 35 self.assertContains(response, 'password', status_code=400)
33 36
user = User.objects.get(email="none@none.com") 34 37 user = User.objects.get(email="none@none.com")
user.is_active = False 35 38 user.is_active = False
user.save() 36 39 user.save()
37 40
data = {'email': 'none@none.com', 'password': '1234'} 38 41 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 39 42 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 40 43 self.assertContains(response, 'Account is disabled', status_code=403)
41 44
def test_logout(self): 42 45 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 43 46 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout') 44 47 response = self.client.post('/api/logout')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 45 48 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
46 49
# since we're not logged in, we should get a 403 response 47 50 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me', format='json') 48 51 response = self.client.get('/api/me', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 49 52 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
50 53
51 54
class PasswordResetTest(APITestCase): 52 55 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 53 56 fixtures = ['testusers']
54 57
def test_reset_password(self): 55 58 def test_reset_password(self):
# submit the request to reset the password 56 59 # submit the request to reset the password
url = '/api/request_password_reset' 57 60 url = '/api/request_password_reset'
post_data = {'email': 'none@none.com'} 58 61 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 59 62 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 60 63 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 61 64 self.assertIn('reset your password', mail.outbox[0].body)
62 65
# capture the reset token from the email 63 66 # capture the reset token from the email
capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', 64 67 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 65 68 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 66 69 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 67 70 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 68 71 reset_token = capture.group(2)
69 72
# try to reset the password with the wrong reset token 70 73 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 71 74 patch_data['token'] = 'wrong_token'
url = '/api/reset_password' 72 75 url = '/api/reset_password'
response = self.client.post(url, patch_data, format='json') 73 76 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 74 77 self.assertContains(response, 'Could not verify reset token', status_code=400)
75 78
# try to reset the password with the correct token 76 79 # try to reset the password with the correct token
patch_data['token'] = reset_token 77 80 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 78 81 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 79 82 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 80 83 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 81 84 assert user.check_password(patch_data['new_password'])
82 85
83 86
class RegistrationTest(APITestCase): 84 87 class RegistrationTest(APITestCase):
def test_create_account(self): 85 88 def test_create_account(self):
url = '/api/register' 86 89 url = '/api/register'
87 90
# missing password 88 91 # missing password
data = {'email': 'none@none.com'} 89 92 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 90 93 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 91 94 self.assertContains(response, 'password', status_code=400)
92 95
# missing email 93 96 # missing email
data = {'password': '1234'} 94 97 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 95 98 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 96 99 self.assertContains(response, 'email', status_code=400)
97 100
# create a user 98 101 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 99 102 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 100 103 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 101 104 self.assertEqual(response.status_code, HTTP_201_CREATED)
102 105
# user should not be confirmed 103 106 # user should not be confirmed
user = User.objects.get(email="none@none.com") 104 107 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 105 108 self.assertFalse(user.is_confirmed)
106 109
# check that the confirmation key was sent 107 110 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 108 111 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 109 112 self.assertIn(user.confirmation_key, mail.outbox[0].body)
110 113
# log the user out 111 114 # log the user out
self.client.logout() 112 115 self.client.logout()
113 116
# log the user in with their registered credentials 114 117 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 115 118 self.client.login(email='none@none.com', password='1234')
116 119
# try activating with an invalid key 117 120 # try activating with an invalid key
118 121
url = '/api/me' 119 122 url = '/api/me'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 120 123 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 121 124 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
122 125
# try activating with the valid key 123 126 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 124 127 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 125 128 self.assertTrue(response.data['is_confirmed'])
126 129
127 130
class ProfileViewTest(APITestCase): 128 131 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 129 132 fixtures = ['testusers']
130 133
def test_get_me(self): 131 134 def test_get_me(self):
url = '/api/me' 132 135 url = '/api/me'
response = self.client.get(url, format='json') 133 136 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 134 137 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 135 138 self.assertEqual(response.status_code, 403)
136 139
self.client.login(email='none@none.com', password='1234') 137 140 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 138 141 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 139 142 self.assertEqual(response.status_code, HTTP_200_OK)
140 143
141 144
class PasswordChangeTest(APITestCase): 142 145 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 143 146 fixtures = ['testusers']
144 147
def test_change_password(self): 145 148 def test_change_password(self):
url = '/api/me' 146 149 url = '/api/me'
user = User.objects.get(email='none@none.com') 147 150 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 148 151 self.assertTrue(user.check_password('1234'))
149 152
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 150 153 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 151 154 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
152 155
self.client.login(email='none@none.com', password='1234') 153 156 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 154 157 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 155 158 self.assertContains(response, 'old_password is required', status_code=400)
156 159
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 157 160 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 158 161 self.assertContains(response, 'old_password is incorrect', status_code=400)
159 162
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 163 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 161 164 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 162 165 user = User.objects.get(email='none@none.com')
163 166
self.assertFalse(user.check_password('1234')) 164 167 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 165 168 self.assertTrue(user.check_password('4321'))
166 169
167 170
class DeleteUserTest(APITestCase): 168 171 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 169 172 fixtures = ['testusers']
170 173
def test_delete_user(self): 171 174 def test_delete_user(self):
url = '/api/me' 172 175 url = '/api/me'
user = User.objects.get(email='none@none.com') 173 176 user = User.objects.get(email='none@none.com')
174 177
self.client.login(email='none@none.com', password='1234') 175 178 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 176 179 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 177 180 self.assertFalse(User.objects.filter(email='none@none.com').exists())
178 181
179 182
class FlashcardDetailTest(APITestCase): 180 183 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 181 184 fixtures = ['testusers', 'testsections']
182 185
def setUp(self): 183 186 def setUp(self):
section = Section.objects.get(pk=1) 184 187 self.section = Section.objects.get(pk=1)
user = User.objects.get(email='none@none.com') 185 188 self.section.save()
186 189 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) 187 190 self.user.sections.add(self.section)
191 self.user.save()
192 self.flashcard = Flashcard(text="jason",
193 section=self.section,
194 material_date=now(),
195 author=self.user)
self.flashcard.save() 188 196 self.flashcard.save()
197 self.flashcard.add_to_deck(self.user)
189 198
def test_edit_flashcard(self): 190 199 def test_edit_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191 200 self.client.login(email='none@none.com', password='1234')
user = User.objects.get(email='none@none.com') 192 201 user = self.user
user.sections.add(Section.objects.get(pk=1)) 193 202 flashcard = self.flashcard
user.save() 194 203 url = "/api/flashcards/{}/".format(flashcard.pk)
204 data = {'text': 'new wow for the flashcard',
205 'mask': '[[0,4]]'}
206 self.assertNotEqual(flashcard.text, data['text'])
207 response = self.client.patch(url, data, format='json')
208 self.assertEqual(response.status_code, HTTP_200_OK)
209 self.assertEqual(response.data['text'], data['text'])
210 data = {'material_date': datetime(2015, 4, 12, 2, 2, 2),
211 'mask': '[[1, 3]]'}
212 user2 = User.objects.create(email='wow@wow.wow', password='wow')
213 user2.sections.add(self.section)
214 user2.save()
215 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
216 response = self.client.patch(url, data, format='json')
217 serializer = FlashcardSerializer(data=response.data)
218 serializer.is_valid(raise_exception=True)
219 self.assertEqual(response.status_code, HTTP_200_OK)
220 # self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date']))
221 self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]]))
222 data = {'mask': '[[3,6]]'}
223 response = self.client.patch(url, data, format='json')
224 user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard)
225 self.assertEqual(response.status_code, HTTP_200_OK)
226 self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]]))
195 227
def test_create_flashcard(self): 196 228 def test_create_flashcard(self):
self.client.login(email='none@none.com', password='1234') 197 229 self.client.login(email='none@none.com', password='1234')
user = User.objects.get(email='none@none.com') 198 230 user = User.objects.get(email='none@none.com')
user.sections.add(Section.objects.get(pk=1)) 199 231 user.sections.add(Section.objects.get(pk=1))
user.save() 200 232 user.save()
data = {'text': 'this is a flashcard', 201 233 data = {'text': 'this is a flashcard',
'material_date': str(datetime.now()), 202 234 'material_date': str(datetime.now()),
'mask': '[]', 203 235 'mask': '[]',
'section': '1', 204 236 'section': '1',
'previous': None} 205 237 'previous': None}
response = self.client.post("/api/flashcards/", data, format="json") 206 238 response = self.client.post("/api/flashcards/", data, format="json")
self.assertEqual(response.status_code, HTTP_201_CREATED) 207 239 self.assertEqual(response.status_code, HTTP_201_CREATED)
self.assertEqual(response.data['text'], data['text']) 208 240 self.assertEqual(response.data['text'], data['text'])
self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists()) 209 241 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
210 242
def test_get_flashcard(self): 211 243 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 212 244 self.client.login(email='none@none.com', password='1234')
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 213 245 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 214 246 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 215 247 self.assertEqual(response.data["text"], "jason")
216 248
217 249
class SectionViewSetTest(APITestCase): 218 250 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 219 251 fixtures = ['testusers', 'testsections']
220 252
def setUp(self): 221 253 def setUp(self):
self.client.login(email='none@none.com', password='1234') 222 254 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 223 255 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user) 224 256 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user)
self.flashcard.save() 225 257 self.flashcard.save()
self.section = Section.objects.get(pk=1) 226 258 self.section = Section.objects.get(pk=1)
227 259
def test_list_sections(self): 228 260 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 229 261 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 230 262 self.assertEqual(response.status_code, HTTP_200_OK)
231 263
def test_section_enroll(self): 232 264 def test_section_enroll(self):
section = self.section 233 265 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 234 266 self.assertFalse(self.user.sections.filter(pk=section.pk))
235 267
# test enrolling in a section without a whitelist 236 268 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 237 269 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 238 270 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 239 271 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
240 272
section = Section.objects.get(pk=2) 241 273 section = Section.objects.get(pk=2)
flashcards/views.py View file @ 5d861cb
from django.contrib import auth 1 1 from django.contrib import auth
from django.shortcuts import get_object_or_404 2 2 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination 3 3 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 4 4 from flashcards.models import Section, User, Flashcard, FlashcardHide, 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, \
FlashcardUpdateSerializer 7 7 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 8 8 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 9 9 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 10 10 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 11 11 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 12 12 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 13 13 from django.core.mail import send_mail
from django.contrib.auth import authenticate 14 14 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 15 15 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 16 16 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 17 17 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 18 18 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 19 19 from simple_email_confirmation import EmailAddress
20 20
21 21
class SectionViewSet(ReadOnlyModelViewSet): 22 22 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 23 23 queryset = Section.objects.all()
serializer_class = SectionSerializer 24 24 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 25 25 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 26 26 permission_classes = [IsAuthenticated]
27 27
@detail_route(methods=['GET']) 28 28 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 29 29 def flashcards(self, request, pk):
""" 30 30 """
Gets flashcards for a section, excluding hidden cards. 31 31 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 32 32 Returned in strictly chronological order (material date).
""" 33 33 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 34 34 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object()).all() 35 35 section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 36 36 return Response(FlashcardSerializer(flashcards, many=True).data)
37 37
@detail_route(methods=['post']) 38 38 @detail_route(methods=['post'])
def enroll(self, request, pk): 39 39 def enroll(self, request, pk):
""" 40 40 """
Add the current user to a specified section 41 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. 42 42 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 43 43 ---
omit_serializer: true 44 44 omit_serializer: true
parameters: 45 45 parameters:
- fake: None 46 46 - fake: None
parameters_strategy: 47 47 parameters_strategy:
form: replace 48 48 form: replace
""" 49 49 """
section = self.get_object() 50 50 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 51 51 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 52 52 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 53 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.") 54 54 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 55 55 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 56 56 return Response(status=HTTP_204_NO_CONTENT)
57 57
@detail_route(methods=['post']) 58 58 @detail_route(methods=['post'])
def drop(self, request, pk): 59 59 def drop(self, request, pk):
""" 60 60 """
Remove the current user from a specified section 61 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 62 If the user is not in the class, the request will fail.
--- 63 63 ---
omit_serializer: true 64 64 omit_serializer: true
parameters: 65 65 parameters:
- fake: None 66 66 - fake: None
parameters_strategy: 67 67 parameters_strategy:
form: replace 68 68 form: replace
""" 69 69 """
section = self.get_object() 70 70 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 71 71 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 72 72 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 73 73 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 74 74 return Response(status=HTTP_204_NO_CONTENT)
75 75
@list_route(methods=['GET']) 76 76 @list_route(methods=['GET'])
def search(self, request): 77 77 def search(self, request):
""" 78 78 """
Returns a list of sections which match a user's query 79 79 Returns a list of sections which match a user's query
""" 80 80 """
query = request.GET.get('q', None) 81 81 query = request.GET.get('q', None)
if not query: return Response('[]') 82 82 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 83 83 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 84 84 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 85 85 return Response(serializer.data)
86 86
@detail_route(methods=['GET']) 87 87 @detail_route(methods=['GET'])
def deck(self, request, pk): 88 88 def deck(self, request, pk):
""" 89 89 """
Gets the contents of a user's deck for a given section. 90 90 Gets the contents of a user's deck for a given section.
""" 91 91 """
qs = request.user.get_deck(self.get_object()) 92 92 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 93 93 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 94 94 return Response(serializer.data)
95 95
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 96 96 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 97 97 def ordered_deck(self, request, pk):
""" 98 98 """
Get a chronological order by material_date of flashcards for a section. 99 99 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 100 100 This excludes hidden card.
""" 101 101 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 102 102 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 103 103 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 104 104 return Response(serializer.data)
105 105
@detail_route(methods=['GET']) 106 106 @detail_route(methods=['GET'])
def feed(self, request, pk): 107 107 def feed(self, request, pk):
""" 108 108 """
Gets the contents of a user's feed for a section. 109 109 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 110 110 Exclude cards that are already in the user's deck
""" 111 111 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 112 112 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 113 113 return Response(serializer.data)
114 114
115 115
class UserSectionListView(ListAPIView): 116 116 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 117 117 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 118 118 permission_classes = [IsAuthenticated]
119 119
def get_queryset(self): 120 120 def get_queryset(self):
return self.request.user.sections.all() 121 121 return self.request.user.sections.all()
122 122
def paginate_queryset(self, queryset): return None 123 123 def paginate_queryset(self, queryset): return None
124 124
125 125
class UserDetail(GenericAPIView): 126 126 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 127 127 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 128 128 permission_classes = [IsAuthenticated]
129 129
def get_queryset(self): 130 130 def get_queryset(self):
return User.objects.all() 131 131 return User.objects.all()
132 132
def patch(self, request, format=None): 133 133 def patch(self, request, format=None):
""" 134 134 """
Updates the user's password, or verifies their email address 135 135 Updates the user's password, or verifies their email address
--- 136 136 ---
request_serializer: UserUpdateSerializer 137 137 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 138 138 response_serializer: UserSerializer
""" 139 139 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 140 140 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 141 141 data.is_valid(raise_exception=True)
data = data.validated_data 142 142 data = data.validated_data
143 143
if 'new_password' in data: 144 144 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 145 145 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 146 146 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 147 147 request.user.set_password(data['new_password'])
request.user.save() 148 148 request.user.save()
149 149
if 'confirmation_key' in data: 150 150 if 'confirmation_key' in data:
try: 151 151 try:
request.user.confirm_email(data['confirmation_key']) 152 152 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 153 153 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 154 154 raise ValidationError('confirmation_key is invalid')
155 155
return Response(UserSerializer(request.user).data) 156 156 return Response(UserSerializer(request.user).data)
157 157
def get(self, request, format=None): 158 158 def get(self, request, format=None):
""" 159 159 """
Return data about the user 160 160 Return data about the user
--- 161 161 ---
response_serializer: UserSerializer 162 162 response_serializer: UserSerializer
""" 163 163 """
serializer = UserSerializer(request.user, context={'request': request}) 164 164 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 165 165 return Response(serializer.data)
166 166
def delete(self, request): 167 167 def delete(self, request):
""" 168 168 """
Irrevocably delete the user and their data 169 169 Irrevocably delete the user and their data
170 170
Yes, really 171 171 Yes, really
""" 172 172 """
request.user.delete() 173 173 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 174 174 return Response(status=HTTP_204_NO_CONTENT)
175 175
176 176
@api_view(['POST']) 177 177 @api_view(['POST'])
def register(request, format=None): 178 178 def register(request, format=None):
""" 179 179 """
Register a new user 180 180 Register a new user
--- 181 181 ---
request_serializer: EmailPasswordSerializer 182 182 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 183 183 response_serializer: UserSerializer
""" 184 184 """
data = RegistrationSerializer(data=request.data) 185 185 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 186 186 data.is_valid(raise_exception=True)
187 187
User.objects.create_user(**data.validated_data) 188 188 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 189 189 user = authenticate(**data.validated_data)
auth.login(request, user) 190 190 auth.login(request, user)
191 191
body = ''' 192 192 body = '''
Visit the following link to confirm your email address: 193 193 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 194 194 https://flashy.cards/app/verifyemail/%s
195 195
If you did not register for Flashy, no action is required. 196 196 If you did not register for Flashy, no action is required.
''' 197 197 '''
198 198
assert send_mail("Flashy email verification", 199 199 assert send_mail("Flashy email verification",
body % user.confirmation_key, 200 200 body % user.confirmation_key,
"noreply@flashy.cards", 201 201 "noreply@flashy.cards",
[user.email]) 202 202 [user.email])
203 203
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 204 204 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
205 205
206 206
@api_view(['POST']) 207 207 @api_view(['POST'])
def login(request): 208 208 def login(request):
""" 209 209 """
Authenticates user and returns user data if valid. 210 210 Authenticates user and returns user data if valid.
--- 211 211 ---
request_serializer: EmailPasswordSerializer 212 212 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 213 213 response_serializer: UserSerializer
""" 214 214 """
215 215
data = EmailPasswordSerializer(data=request.data) 216 216 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 217 217 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 218 218 user = authenticate(**data.validated_data)
219 219
if user is None: 220 220 if user is None:
raise AuthenticationFailed('Invalid email or password') 221 221 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 222 222 if not user.is_active:
raise NotAuthenticated('Account is disabled') 223 223 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 224 224 auth.login(request, user)
return Response(UserSerializer(request.user).data) 225 225 return Response(UserSerializer(request.user).data)
226 226
227 227
@api_view(['POST']) 228 228 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 229 229 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 230 230 def logout(request, format=None):
""" 231 231 """
Logs the authenticated user out. 232 232 Logs the authenticated user out.
""" 233 233 """
auth.logout(request) 234 234 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 235 235 return Response(status=HTTP_204_NO_CONTENT)
236 236
237 237
@api_view(['POST']) 238 238 @api_view(['POST'])
def request_password_reset(request, format=None): 239 239 def request_password_reset(request, format=None):
""" 240 240 """
Send a password reset token/link to the provided email. 241 241 Send a password reset token/link to the provided email.
--- 242 242 ---
request_serializer: PasswordResetRequestSerializer 243 243 request_serializer: PasswordResetRequestSerializer
""" 244 244 """
data = PasswordResetRequestSerializer(data=request.data) 245 245 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 246 246 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 247 247 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 248 248 token = default_token_generator.make_token(user)
249 249
body = ''' 250 250 body = '''
Visit the following link to reset your password: 251 251 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 252 252 https://flashy.cards/app/resetpassword/%d/%s
253 253
If you did not request a password reset, no action is required. 254 254 If you did not request a password reset, no action is required.
''' 255 255 '''
256 256
send_mail("Flashy password reset", 257 257 send_mail("Flashy password reset",
body % (user.pk, token), 258 258 body % (user.pk, token),
"noreply@flashy.cards", 259 259 "noreply@flashy.cards",
[user.email]) 260 260 [user.email])
261 261
return Response(status=HTTP_204_NO_CONTENT) 262 262 return Response(status=HTTP_204_NO_CONTENT)
263 263
264 264
@api_view(['POST']) 265 265 @api_view(['POST'])
def reset_password(request, format=None): 266 266 def reset_password(request, format=None):
""" 267 267 """
Updates user's password to new password if token is valid. 268 268 Updates user's password to new password if token is valid.
--- 269 269 ---
request_serializer: PasswordResetSerializer 270 270 request_serializer: PasswordResetSerializer
""" 271 271 """
data = PasswordResetSerializer(data=request.data) 272 272 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 273 273 data.is_valid(raise_exception=True)
274 274
user = User.objects.get(id=data['uid'].value) 275 275 user = User.objects.get(id=data['uid'].value)
# Check token validity. 276 276 # Check token validity.
277 277
if default_token_generator.check_token(user, data['token'].value): 278 278 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 279 279 user.set_password(data['new_password'].value)
user.save() 280 280 user.save()
else: 281 281 else:
raise ValidationError('Could not verify reset token') 282 282 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 283 283 return Response(status=HTTP_204_NO_CONTENT)
284 284
285 285
class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): 286 286 class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 287 287 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 288 288 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 289 289 permission_classes = [IsAuthenticated]
290 290
# Override create in CreateModelMixin 291 291 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 292 292 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 293 293 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 294 294 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 295 295 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 296 296 if not request.user.is_in_section(data['section']):
raise PermissionDenied("You have to be enrolled in this section to add a flashcard") 297 297 raise PermissionDenied("You have to be enrolled in this section to add a flashcard")
data['author'] = request.user 298 298 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 299 299 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 300 300 self.perform_create(flashcard)
headers = self.get_success_headers(data) 301 301 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard) 302 302 response_data = FlashcardSerializer(flashcard)
return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) 303 303 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
304 304
@detail_route(methods=['post']) 305 305 @detail_route(methods=['post'])
def hide(self, request, pk): 306 306 def hide(self, request, pk):
""" 307 307 """
Hide a flashcard 308 308 Hide a flashcard
--- 309 309 ---
omit_serializer: true 310 310 omit_serializer: true
parameters: 311 311 parameters:
- fake: None 312 312 - fake: None
parameters_strategy: 313 313 parameters_strategy:
form: replace 314 314 form: replace
""" 315 315 """
obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) 316 316 obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object())
if not created: 317 317 if not created:
raise ValidationError("The card has already been hidden.") 318 318 raise ValidationError("The card has already been hidden.")
319 319
obj.save() 320 320 obj.save()
return Response(status=HTTP_204_NO_CONTENT) 321 321 return Response(status=HTTP_204_NO_CONTENT)
322 322
@detail_route(methods=['post']) 323 323 @detail_route(methods=['post'])
def unhide(self, request, pk): 324 324 def unhide(self, request, pk):
""" 325 325 """
Report the given card 326 326 Report the given card
--- 327 327 ---
omit_serializer: true 328 328 omit_serializer: true
parameters: 329 329 parameters:
- fake: None 330 330 - fake: None
parameters_strategy: 331 331 parameters_strategy:
form: replace 332 332 form: replace
""" 333 333 """
hide = get_object_or_404(FlashcardHide ,user=request.user, flashcard=self.get_object()) 334 334 hide = get_object_or_404(FlashcardHide ,user=request.user, flashcard=self.get_object())
hide.delete() 335 335 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 336 336 return Response(status=HTTP_204_NO_CONTENT)
337 337
@detail_route(methods=['post']) 338 338 @detail_route(methods=['post'])
def report(self, request, pk): 339 339 def report(self, request, pk):
""" 340 340 """
Report the given card 341 341 Report the given card