Commit 1c546f804757a9ccbef30c441591b15456f76752

Authored by Andrew Buss
1 parent 7031375881
Exists in master

cleaned up some tests

Showing 3 changed files with 41 additions and 57 deletions Inline Diff

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