Commit c91a932f6e439e3c9e9170823bad3073d10433d4

Authored by Chung Wang
Exists in master

Merge remote-tracking branch 'origin/master'

Conflicts:
	flashcards/tests/test_api.py

Showing 6 changed files Inline Diff

flashcards/models.py View file @ c91a932
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
from django.db.models import * 5 5 from django.db.models import *
from django.utils.timezone import now 6 6 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 7 7 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 8 8 from fields import MaskField
9 9
10 10
# Hack to fix AbstractUser before subclassing it 11 11 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 12 12 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 13 13 AbstractUser._meta.get_field('username')._unique = False
14 14
15 15
class EmailOnlyUserManager(UserManager): 16 16 class EmailOnlyUserManager(UserManager):
""" 17 17 """
A tiny extension of Django's UserManager which correctly creates users 18 18 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 19 19 without usernames (using emails instead).
""" 20 20 """
21 21
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 22 22 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 23 23 """
Creates and saves a User with the given email and password. 24 24 Creates and saves a User with the given email and password.
""" 25 25 """
email = self.normalize_email(email) 26 26 email = self.normalize_email(email)
user = self.model(email=email, 27 27 user = self.model(email=email,
is_staff=is_staff, is_active=True, 28 28 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 29 29 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 30 30 date_joined=now(), **extra_fields)
user.set_password(password) 31 31 user.set_password(password)
user.save(using=self._db) 32 32 user.save(using=self._db)
return user 33 33 return user
34 34
def create_user(self, email, password=None, **extra_fields): 35 35 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 36 36 return self._create_user(email, password, False, False, **extra_fields)
37 37
def create_superuser(self, email, password, **extra_fields): 38 38 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 39 39 return self._create_user(email, password, True, True, **extra_fields)
40 40
41 41
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 42 42 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 43 43 """
An extension of Django's default user model. 44 44 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 45 45 We use email as the username field, and include enrolled sections here
""" 46 46 """
objects = EmailOnlyUserManager() 47 47 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 48 48 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 49 49 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 50 50 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
51 51
def is_in_section(self, section): 52 52 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 53 53 return self.sections.filter(pk=section.pk).exists()
54 54
def pull(self, flashcard): 55 55 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 56 56 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 57 57 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 58 58 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 59 59 user_card.pulled = datetime.now()
user_card.save() 60 60 user_card.save()
61 61
def get_deck(self, section): 62 62 def get_deck(self, section):
if not self.is_in_section(section): 63 63 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 64 64 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 65 65 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
66 66
67 67
class UserFlashcard(Model): 68 68 class UserFlashcard(Model):
""" 69 69 """
Represents the relationship between a user and a flashcard by: 70 70 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 71 71 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 72 72 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 73 73 3. A user has a flashcard hidden from them
""" 74 74 """
user = ForeignKey('User') 75 75 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 76 76 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 77 77 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 78 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 79 79 flashcard = ForeignKey('Flashcard')
80 80
class Meta: 81 81 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 82 82 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 83 83 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 84 84 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 85 85 # By default, order by most recently pulled
ordering = ['-pulled'] 86 86 ordering = ['-pulled']
87 87
88 88
class FlashcardHide(Model): 89 89 class FlashcardHide(Model):
""" 90 90 """
Represents the property of a flashcard being hidden by a user. 91 91 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 92 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 93 93 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 94 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 95 95 """
user = ForeignKey('User') 96 96 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 97 97 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 98 98 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 99 99 hidden = DateTimeField(auto_now_add=True)
100 100
class Meta: 101 101 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 102 102 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 103 103 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 104 104 index_together = ["user", "flashcard"]
105 105
106 106
class Flashcard(Model): 107 107 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 108 108 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 109 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 110 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 111 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 112 112 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 113 113 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 114 114 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 115 115 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 116 116 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") 117 117 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
118 118
class Meta: 119 119 class Meta:
# By default, order by most recently pushed 120 120 # By default, order by most recently pushed
ordering = ['-pushed'] 121 121 ordering = ['-pushed']
122 122
def is_hidden_from(self, user): 123 123 def is_hidden_from(self, user):
""" 124 124 """
A card can be hidden globally, but if a user has the card in their deck, 125 125 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 126 126 this visibility overrides a global hide.
:param user: 127 127 :param user:
:return: Whether the card is hidden from the user. 128 128 :return: Whether the card is hidden from the user.
""" 129 129 """
result = user.userflashcard_set.filter(flashcard=self) 130 130 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 131 131 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 132 132 return result[0].is_hidden()
133 133
def edit(self, user, new_flashcard): 134 134 def edit(self, user, new_flashcard):
""" 135 135 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 136 136 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 137 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 138 138 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 139 139 :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 140 140 """
if not user.is_in_section(self.section): 141 141 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to edit this card") 142 142 raise PermissionDenied("You don't have the permission to edit this card")
143 143
# content_changed is True iff either material_date or text were changed 144 144 # content_changed is True iff either material_date or text were changed
content_changed = False 145 145 content_changed = False
# create_new is True iff the user editing this card is the author of this card 146 146 # 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 147 # and there are no other users with this card in their decks
create_new = user != self.author or \ 148 148 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 149 149 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
150 150
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 151 151 if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
content_changed |= True 152 152 content_changed |= True
self.material_date = new_flashcard['material_date'] 153 153 self.material_date = new_flashcard['material_date']
if 'text' in new_flashcard and self.text != new_flashcard['text']: 154 154 if 'text' in new_flashcard and self.text != new_flashcard['text']:
content_changed |= True 155 155 content_changed |= True
self.text = new_flashcard['text'] 156 156 self.text = new_flashcard['text']
if create_new and content_changed: 157 157 if create_new and content_changed:
self.pk = None 158 158 self.pk = None
if 'mask' in new_flashcard: 159 159 if 'mask' in new_flashcard:
self.mask = new_flashcard['mask'] 160 160 self.mask = new_flashcard['mask']
self.save() 161 161 self.save()
162 162
@classmethod 163 163 @classmethod
def cards_visible_to(cls, user): 164 164 def cards_visible_to(cls, user):
""" 165 165 """
:param user: 166 166 :param user:
:return: A queryset with all cards that should be visible to a user. 167 167 :return: A queryset with all cards that should be visible to a user.
""" 168 168 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 169 169 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
170 170
171 171
class UserFlashcardQuiz(Model): 172 172 class UserFlashcardQuiz(Model):
""" 173 173 """
An event of a user being quizzed on a flashcard. 174 174 An event of a user being quizzed on a flashcard.
""" 175 175 """
user_flashcard = ForeignKey(UserFlashcard) 176 176 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 177 177 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 178 178 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 179 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 180 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
181 181
def status(self): 182 182 def status(self):
""" 183 183 """
There are three stages of a quiz object: 184 184 There are three stages of a quiz object:
1. the user has been shown the card 185 185 1. the user has been shown the card
2. the user has answered the card 186 186 2. the user has answered the card
3. the user has self-evaluated their response's correctness 187 187 3. the user has self-evaluated their response's correctness
188 188
:return: string (evaluated, answered, viewed) 189 189 :return: string (evaluated, answered, viewed)
""" 190 190 """
if self.correct is not None: return "evaluated" 191 191 if self.correct is not None: return "evaluated"
if self.response: return "answered" 192 192 if self.response: return "answered"
return "viewed" 193 193 return "viewed"
194 194
195 195
class Section(Model): 196 196 class Section(Model):
""" 197 197 """
A UCSD course taught by an instructor during a quarter. 198 198 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 199 199 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 200 We index gratuitously to support autofill and because this is primarily read-only
""" 201 201 """
department = CharField(db_index=True, max_length=50) 202 202 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 203 203 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 204 204 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 205 205 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 206 206 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 207 207 quarter = CharField(db_index=True, max_length=4)
208 208
@classmethod 209 209 @classmethod
def search(cls, terms): 210 210 def search(cls, terms):
""" 211 211 """
Search all fields of all sections for a particular set of terms 212 212 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 213 A matching section must match at least one field on each term
:param terms:iterable 214 214 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 215 215 :return: Matching QuerySet ordered by department and course number
""" 216 216 """
final_q = Q() 217 217 final_q = Q()
for term in terms: 218 218 for term in terms:
q = Q(department__icontains=term) 219 219 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 220 220 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 221 221 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 222 222 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 223 223 q |= Q(instructor__icontains=term)
final_q &= q 224 224 final_q &= q
qs = cls.objects.filter(final_q) 225 225 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 226 226 # 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 227 # 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 228 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 229 229 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 230 230 return qs
231 231
@property 232 232 @property
def is_whitelisted(self): 233 233 def is_whitelisted(self):
""" 234 234 """
:return: whether a whitelist exists for this section 235 235 :return: whether a whitelist exists for this section
""" 236 236 """
return self.whitelist.exists() 237 237 return self.whitelist.exists()
238 238
def is_user_on_whitelist(self, user): 239 239 def is_user_on_whitelist(self, user):
""" 240 240 """
:return: whether the user is on the waitlist for this section 241 241 :return: whether the user is on the waitlist for this section
""" 242 242 """
return self.whitelist.filter(email=user.email).exists() 243 243 return self.whitelist.filter(email=user.email).exists()
244 244
class Meta: 245 245 class Meta:
ordering = ['-course_title'] 246 246 ordering = ['-course_title']
247 247
@property 248 248 @property
def lecture_times(self): 249 249 def lecture_times(self):
lecture_periods = self.lectureperiod_set.all() 250 250 lecture_periods = self.lectureperiod_set.all()
if not lecture_periods.exists(): return '' 251 251 if not lecture_periods.exists(): return ''
flashcards/serializers.py View file @ c91a932
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()
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).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():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 144 144 raise serializers.ValidationError("Invalid creation date for the Flashcard")
flashcards/tests/test_api.py View file @ c91a932
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 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN
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 7
8 8
class LoginTests(APITestCase): 9 9 class LoginTests(APITestCase):
fixtures = ['testusers'] 10 10 fixtures = ['testusers']
11 11
def test_login(self): 12 12 def test_login(self):
url = '/api/login' 13 13 url = '/api/login'
data = {'email': 'none@none.com', 'password': '1234'} 14 14 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 15 15 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 16 16 self.assertEqual(response.status_code, HTTP_200_OK)
17 17
data = {'email': 'none@none.com', 'password': '4321'} 18 18 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 19 19 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 20 20 self.assertContains(response, 'Invalid email or password', status_code=403)
21 21
data = {'email': 'bad@none.com', 'password': '1234'} 22 22 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 23 23 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 24 24 self.assertContains(response, 'Invalid email or password', status_code=403)
25 25
data = {'password': '4321'} 26 26 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 27 27 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 28 28 self.assertContains(response, 'email', status_code=400)
29 29
data = {'email': 'none@none.com'} 30 30 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 31 31 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 32 32 self.assertContains(response, 'password', status_code=400)
33 33
user = User.objects.get(email="none@none.com") 34 34 user = User.objects.get(email="none@none.com")
user.is_active = False 35 35 user.is_active = False
user.save() 36 36 user.save()
37 37
data = {'email': 'none@none.com', 'password': '1234'} 38 38 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 39 39 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 40 40 self.assertContains(response, 'Account is disabled', status_code=403)
41 41
def test_logout(self): 42 42 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 43 43 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout') 44 44 response = self.client.post('/api/logout')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 45 45 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
46 46
# since we're not logged in, we should get a 403 response 47 47 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me', format='json') 48 48 response = self.client.get('/api/me', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 49 49 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
50 50
51 51
class PasswordResetTest(APITestCase): 52 52 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 53 53 fixtures = ['testusers']
54 54
def test_reset_password(self): 55 55 def test_reset_password(self):
# submit the request to reset the password 56 56 # submit the request to reset the password
url = '/api/request_password_reset' 57 57 url = '/api/request_password_reset'
post_data = {'email': 'none@none.com'} 58 58 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 59 59 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 60 60 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 61 61 self.assertIn('reset your password', mail.outbox[0].body)
62 62
# capture the reset token from the email 63 63 # capture the reset token from the email
capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', 64 64 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 65 65 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 66 66 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 67 67 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 68 68 reset_token = capture.group(2)
69 69
# try to reset the password with the wrong reset token 70 70 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 71 71 patch_data['token'] = 'wrong_token'
url = '/api/reset_password' 72 72 url = '/api/reset_password'
response = self.client.post(url, patch_data, format='json') 73 73 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 74 74 self.assertContains(response, 'Could not verify reset token', status_code=400)
75 75
# try to reset the password with the correct token 76 76 # try to reset the password with the correct token
patch_data['token'] = reset_token 77 77 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 78 78 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 79 79 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 80 80 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 81 81 assert user.check_password(patch_data['new_password'])
82 82
83 83
class RegistrationTest(APITestCase): 84 84 class RegistrationTest(APITestCase):
def test_create_account(self): 85 85 def test_create_account(self):
url = '/api/register' 86 86 url = '/api/register'
87 87
# missing password 88 88 # missing password
data = {'email': 'none@none.com'} 89 89 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 90 90 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 91 91 self.assertContains(response, 'password', status_code=400)
92 92
# missing email 93 93 # missing email
data = {'password': '1234'} 94 94 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 95 95 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 96 96 self.assertContains(response, 'email', status_code=400)
97 97
# create a user 98 98 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 99 99 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 100 100 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 101 101 self.assertEqual(response.status_code, HTTP_201_CREATED)
102 102
# user should not be confirmed 103 103 # user should not be confirmed
user = User.objects.get(email="none@none.com") 104 104 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 105 105 self.assertFalse(user.is_confirmed)
106 106
# check that the confirmation key was sent 107 107 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 108 108 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 109 109 self.assertIn(user.confirmation_key, mail.outbox[0].body)
110 110
# log the user out 111 111 # log the user out
self.client.logout() 112 112 self.client.logout()
113 113
# log the user in with their registered credentials 114 114 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 115 115 self.client.login(email='none@none.com', password='1234')
116 116
# try activating with an invalid key 117 117 # try activating with an invalid key
118 118
url = '/api/me' 119 119 url = '/api/me'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 120 120 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 121 121 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
122 122
# try activating with the valid key 123 123 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 124 124 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 125 125 self.assertTrue(response.data['is_confirmed'])
126 126
127 127
class ProfileViewTest(APITestCase): 128 128 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 129 129 fixtures = ['testusers']
130 130
def test_get_me(self): 131 131 def test_get_me(self):
url = '/api/me' 132 132 url = '/api/me'
response = self.client.get(url, format='json') 133 133 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 134 134 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 135 135 self.assertEqual(response.status_code, 403)
136 136
self.client.login(email='none@none.com', password='1234') 137 137 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 138 138 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 139 139 self.assertEqual(response.status_code, HTTP_200_OK)
140 140
141 141
class PasswordChangeTest(APITestCase): 142 142 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 143 143 fixtures = ['testusers']
144 144
def test_change_password(self): 145 145 def test_change_password(self):
url = '/api/me' 146 146 url = '/api/me'
user = User.objects.get(email='none@none.com') 147 147 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 148 148 self.assertTrue(user.check_password('1234'))
149 149
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 150 150 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 151 151 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
152 152
self.client.login(email='none@none.com', password='1234') 153 153 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 154 154 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 155 155 self.assertContains(response, 'old_password is required', status_code=400)
156 156
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 157 157 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 158 158 self.assertContains(response, 'old_password is incorrect', status_code=400)
159 159
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 160 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 161 161 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 162 162 user = User.objects.get(email='none@none.com')
163 163
self.assertFalse(user.check_password('1234')) 164 164 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 165 165 self.assertTrue(user.check_password('4321'))
166 166
167 167
class DeleteUserTest(APITestCase): 168 168 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 169 169 fixtures = ['testusers']
170 170
def test_delete_user(self): 171 171 def test_delete_user(self):
url = '/api/me' 172 172 url = '/api/me'
user = User.objects.get(email='none@none.com') 173 173 user = User.objects.get(email='none@none.com')
174 174
self.client.login(email='none@none.com', password='1234') 175 175 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 176 176 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 177 177 self.assertFalse(User.objects.filter(email='none@none.com').exists())
178 178
179 179
class FlashcardDetailTest(APITestCase): 180 180 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 181 181 fixtures = ['testusers', 'testsections']
182 182
def setUp(self): 183 183 def setUp(self):
section = Section.objects.get(pk=1) 184 184 section = Section.objects.get(pk=1)
user = User.objects.get(email='none@none.com') 185 185 user = User.objects.get(email='none@none.com')
186 186
self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) 187 187 self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user)
self.flashcard.save() 188 188 self.flashcard.save()
189 189
def test_get_flashcard(self): 190 190 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191 191 self.client.login(email='none@none.com', password='1234')
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 192 192 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 193 193 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 194 194 self.assertEqual(response.data["text"], "jason")
195 195
196 196
class SectionViewSetTest(APITestCase): 197 197 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 198 198 fixtures = ['testusers', 'testsections']
199 199
def setUp(self): 200 200 def setUp(self):
self.client.login(email='none@none.com', password='1234') 201 201 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 202 202 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), 203 203 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user)
author=self.user) 204
self.flashcard.save() 205 204 self.flashcard.save()
self.section = Section.objects.get(pk=1) 206 205 self.section = Section.objects.get(pk=1)
207 206
def test_list_sections(self): 208 207 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 209 208 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 210 209 self.assertEqual(response.status_code, HTTP_200_OK)
211 210
def test_section_enroll(self): 212 211 def test_section_enroll(self):
section = self.section 213 212 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 214 213 self.assertFalse(self.user.sections.filter(pk=section.pk))
215 214
# test enrolling in a section without a whitelist 216 215 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 217 216 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 218 217 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 219 218 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
220 219
section = Section.objects.get(pk=2) 221 220 section = Section.objects.get(pk=2)
WhitelistedAddress.objects.create(email='bad@none.com', section=section) 222 221 WhitelistedAddress.objects.create(email='bad@none.com', section=section)
223 222
# test enrolling in a section when not on the whitelist 224 223 # test enrolling in a section when not on the whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 225 224 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 226 225 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertFalse(self.user.sections.filter(pk=section.pk).exists()) 227 226 self.assertFalse(self.user.sections.filter(pk=section.pk).exists())
228 227
flashcards/tests/test_models.py View file @ c91a932
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard, UserFlashcard 4 4 from flashcards.models import User, Section, Flashcard, UserFlashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
6 6
7 7
class RegistrationTests(TestCase): 8 8 class RegistrationTests(TestCase):
def setUp(self): 9 9 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 10 10 User.objects.create_user(email="none@none.com", password="1234")
11 11
def test_email_confirmation(self): 12 12 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 13 13 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 14 14 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 15 15 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 16 16 self.assertTrue(user.is_confirmed)
17 17
18 18
class UserTests(TestCase): 19 19 class UserTests(TestCase):
def setUp(self): 20 20 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 21 21 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 22 22 Section.objects.create(department='dept',
course_num='101a', 23 23 course_num='101a',
course_title='how 2 test', 24 24 course_title='how 2 test',
instructor='George Lucas', 25 25 instructor='George Lucas',
quarter='SP15') 26 26 quarter='SP15')
27 27
def test_section_list(self): 28 28 def test_section_list(self):
section = Section.objects.get(course_num='101a') 29 29 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 30 30 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 31 31 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 32 32 user.sections.add(section)
self.assertIn(section, user.sections.all()) 33 33 self.assertIn(section, user.sections.all())
user.sections.add(section) 34 34 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 35 35 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 36 36 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 37 37 self.assertEqual(user.sections.count(), 0)
38 38
39 39
class FlashcardMaskTest(TestCase): 40 40 class FlashcardMaskTest(TestCase):
41 def test_empty(self):
42 try:
43 fm = FlashcardMask([])
44 self.assertEqual(fm.max_offset(), -1)
45 except TypeError:
46 self.fail()
47 try:
48 fm = FlashcardMask('')
49 self.assertEqual(fm.max_offset(), -1)
50 except TypeError:
51 self.fail()
52 try:
53 fm = FlashcardMask(None)
54 self.assertEqual(fm.max_offset(), -1)
55 except TypeError:
56 self.fail()
57
def test_iterable(self): 41 58 def test_iterable(self):
try: 42 59 try:
FlashcardMask(1) 43 60 FlashcardMask(1)
except TypeError as te: 44 61 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 45 62 self.assertEqual(te.message, "Interval not a valid iterable")
try: 46 63 try:
FlashcardMask([1, 2, 4]) 47 64 FlashcardMask([1, 2, 4])
except TypeError as te: 48 65 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 49 66 self.assertEqual(te.message, "Interval not a valid iterable")
50 67
def test_interval(self): 51 68 def test_interval(self):
try: 52 69 try:
FlashcardMask([[1, 2, 3], [1]]) 53 70 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 54 71 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 55 72 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 56 73 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 57 74 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 58 75 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 59 76 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 60 77 try:
FlashcardMask(([1, 2], [1])) 61 78 FlashcardMask(([1, 2], [1]))
except TypeError as te: 62 79 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 63 80 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 64 81 try:
FlashcardMask("[1,2,3]") 65 82 FlashcardMask("[1,2,3]")
except TypeError as te: 66 83 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 67 84 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
68 85
def test_overlap(self): 69 86 def test_overlap(self):
try: 70 87 try:
FlashcardMask({(1, 2), (2, 5)}) 71 88 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 72 89 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 73 90 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 74 91 try:
FlashcardMask({(1, 20), (12, 15)}) 75 92 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 76 93 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 77 94 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 78 95 try:
FlashcardMask({(2, 1), (5, 2)}) 79 96 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 80 97 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 81 98 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
82 99
83 100
84 101
class FlashcardTests(TestCase): 85 102 class FlashcardTests(TestCase):
def setUp(self): 86 103 def setUp(self):
section = Section.objects.create(department='dept', 87 104 section = Section.objects.create(department='dept',
course_num='101a', 88 105 course_num='101a',
course_title='how 2 test', 89 106 course_title='how 2 test',
instructor='George Lucas', 90 107 instructor='George Lucas',
quarter='SP15') 91 108 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 92 109 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 93 110 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 94 111 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 95 112 section=section,
author=user, 96 113 author=user,
material_date=datetime.now(), 97 114 material_date=datetime.now(),
previous=None, 98 115 previous=None,
mask={(24,34), (0, 4)}) 99 116 mask={(24,34), (0, 4)})
user.save() 100 117 user.save()
section.save() 101 118 section.save()
flashcard.save() 102 119 flashcard.save()
103 120
def test_flashcard_edit(self): 104 121 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 105 122 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 106 123 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 107 124 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 108 125 user2.sections.add(section)
user2.save() 109 126 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 110 127 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 111 128 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 112 129 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 113 130 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 114 131 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 115 132 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 116 133 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 117 134 self.assertNotEqual(flashcard.pk, pk_backup)
flashcards/validators.py View file @ c91a932
from collections import Iterable 1 1 from collections import Iterable
2 2
3 3
class FlashcardMask(set): 4 4 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 5 5 def __init__(self, iterable, *args, **kwargs):
6 if iterable is None or iterable == '':
7 iterable = []
self._iterable_check(iterable) 6 8 self._iterable_check(iterable)
iterable = map(tuple, iterable) 7 9 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 8 10 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 9 11 self._interval_check()
self._overlap_check() 10 12 self._overlap_check()
11 13
def max_offset(self): 12 14 def max_offset(self):
return self._end 13 15 return self._end
14 16
def _iterable_check(self, iterable): 15 17 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 16 18 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 17 19 raise TypeError("Interval not a valid iterable")
18 20
def _interval_check(self): 19 21 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 20 22 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 21 23 raise TypeError("Intervals must have exactly 2 elements, begin and end")
22 24
def _overlap_check(self): 23 25 def _overlap_check(self):
p_beg, p_end = -1, -1 24 26 p_beg, p_end = -1, -1
for interval in sorted(self): 25 27 for interval in sorted(self):
beg, end = map(int, interval) 26 28 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 27 29 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") 28 30 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 29 31 p_beg, p_end = beg, end
self._end = p_end 30 32 self._end = p_end
flashcards/views.py View file @ c91a932
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
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 = self.get_serializer(data=request.data) 293 293 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 294 294 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 295 295 data = serializer.validated_data
self.perform_create(serializer) 296 296 if not request.user.is_in_section(data['section']):
headers = self.get_success_headers(serializer.data) 297 297 raise PermissionDenied("You have to be enrolled in this section to add a flashcard")
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 298 298 data['author'] = request.user
299 flashcard = Flashcard.objects.create(**data)
300 self.perform_create(flashcard)
301 headers = self.get_success_headers(data)
302 response_data = FlashcardSerializer(flashcard)
303 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
299 304
@detail_route(methods=['post']) 300 305 @detail_route(methods=['post'])
def hide(self, request, pk): 301 306 def hide(self, request, pk):
""" 302 307 """
Hide a flashcard 303 308 Hide a flashcard
--- 304 309 ---
omit_serializer: true 305 310 omit_serializer: true
parameters: 306 311 parameters:
- fake: None 307 312 - fake: None
parameters_strategy: 308 313 parameters_strategy:
form: replace 309 314 form: replace
""" 310 315 """
obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) 311 316 obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object())
if not created: 312 317 if not created:
raise ValidationError("The card has already been hidden.") 313 318 raise ValidationError("The card has already been hidden.")
314 319
obj.save() 315 320 obj.save()
return Response(status=HTTP_204_NO_CONTENT) 316 321 return Response(status=HTTP_204_NO_CONTENT)
317 322
@detail_route(methods=['post']) 318 323 @detail_route(methods=['post'])
def unhide(self, request, pk): 319 324 def unhide(self, request, pk):
""" 320 325 """
Report the given card 321 326 Report the given card
--- 322 327 ---
omit_serializer: true 323 328 omit_serializer: true
parameters: 324 329 parameters:
- fake: None 325 330 - fake: None
parameters_strategy: 326 331 parameters_strategy:
form: replace 327 332 form: replace
""" 328 333 """
hide = get_object_or_404(FlashcardHide ,user=request.user, flashcard=self.get_object()) 329 334 hide = get_object_or_404(FlashcardHide ,user=request.user, flashcard=self.get_object())
hide.delete() 330 335 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 331 336 return Response(status=HTTP_204_NO_CONTENT)
332 337