Commit 594f44ae0a971980d61c2c1868136ccce4ab78cb

Authored by Rohan Rangray
Exists in master

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

Mergin study view fixes.

Showing 2 changed files Inline Diff

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