Commit f75aee4a9b3a13ab914bdd8f3672d94694e1f4b8

Authored by Andrew Buss
1 parent a3ae545efe
Exists in master

fix editflashcard test

Showing 2 changed files with 5 additions and 4 deletions Inline Diff

flashcards/models.py View file @ f75aee4
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
from datetime import timedelta 3 3 from datetime import timedelta
4 4
from flashcards.validators import FlashcardMask 5 5 from flashcards.validators import FlashcardMask
from gcm import gcm 6 6 from gcm import gcm
from django.contrib.auth.models import AbstractUser, UserManager 7 7 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 8 8 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 9 9 from django.core.cache import cache
from django.core.exceptions import ValidationError 10 10 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied 11 11 from django.core.exceptions import PermissionDenied
from django.core.mail import send_mail 12 12 from django.core.mail import send_mail
from django.core.validators import MinLengthValidator 13 13 from django.core.validators import MinLengthValidator
from django.db import IntegrityError 14 14 from django.db import IntegrityError
from django.db.models import * 15 15 from django.db.models import *
from django.utils.timezone import now, make_aware 16 16 from django.utils.timezone import now, make_aware
from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY 17 17 from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY
from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress 18 18 from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress
from fields import MaskField 19 19 from fields import MaskField
from cached_property import cached_property 20 20 from cached_property import cached_property
21 21
22 22
23 23
24 24
25 25
26 26
27 27
28 28
29 29
30 30
31 31
32 32
33 33
34 34
# Hack to fix AbstractUser before subclassing it 35 35 # Hack to fix AbstractUser before subclassing it
36 36
AbstractUser._meta.get_field('email')._unique = True 37 37 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 38 38 AbstractUser._meta.get_field('username')._unique = False
39 39
40 40
class EmailOnlyUserManager(UserManager): 41 41 class EmailOnlyUserManager(UserManager):
""" 42 42 """
A tiny extension of Django's UserManager which correctly creates users 43 43 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 44 44 without usernames (using emails instead).
""" 45 45 """
46 46
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 47 47 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 48 48 """
Creates and saves a User with the given email and password. 49 49 Creates and saves a User with the given email and password.
""" 50 50 """
email = self.normalize_email(email) 51 51 email = self.normalize_email(email)
user = self.model(email=email, 52 52 user = self.model(email=email,
is_staff=is_staff, is_active=True, 53 53 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 54 54 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 55 55 date_joined=now(), **extra_fields)
user.set_password(password) 56 56 user.set_password(password)
user.save(using=self._db) 57 57 user.save(using=self._db)
user.send_confirmation_email() 58 58 user.send_confirmation_email()
return user 59 59 return user
60 60
def create_user(self, email, password=None, **extra_fields): 61 61 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 62 62 user = self._create_user(email, password, False, False, **extra_fields)
63 63
return user 64 64 return user
65 65
def create_superuser(self, email, password, **extra_fields): 66 66 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 67 67 return self._create_user(email, password, True, True, **extra_fields)
68 68
69 69
class FlashcardAlreadyPulledException(Exception): 70 70 class FlashcardAlreadyPulledException(Exception):
pass 71 71 pass
72 72
73 73
class FlashcardNotInDeckException(Exception): 74 74 class FlashcardNotInDeckException(Exception):
pass 75 75 pass
76 76
77 77
class FlashcardNotHiddenException(Exception): 78 78 class FlashcardNotHiddenException(Exception):
pass 79 79 pass
80 80
81 81
class FlashcardAlreadyHiddenException(Exception): 82 82 class FlashcardAlreadyHiddenException(Exception):
pass 83 83 pass
84 84
85 85
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 86 86 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 87 87 """
An extension of Django's default user model. 88 88 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 89 89 We use email as the username field, and include enrolled sections here
""" 90 90 """
objects = EmailOnlyUserManager() 91 91 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 92 92 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 93 93 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 94 94 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 95 95 confirmed_email = BooleanField(default=False)
registration_id = CharField(null=True, default=None, max_length=4096) 96 96 registration_id = CharField(null=True, default=None, max_length=4096)
last_notified = DateTimeField(null=True, default=None) 97 97 last_notified = DateTimeField(null=True, default=None)
98 98
@property 99 99 @property
def locked(self): 100 100 def locked(self):
if self.confirmed_email: return False 101 101 if self.confirmed_email: return False
return (now() - self.date_joined).days > 0 102 102 return (now() - self.date_joined).days > 0
103 103
def send_confirmation_email(self): 104 104 def send_confirmation_email(self):
body = ''' 105 105 body = '''
Visit the following link to confirm your email address: 106 106 Visit the following link to confirm your email address:
%sapp/verifyemail/%s 107 107 %sapp/verifyemail/%s
108 108
If you did not register for Flashy, no action is required. 109 109 If you did not register for Flashy, no action is required.
''' 110 110 '''
send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key), 111 111 send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key),
"noreply@flashy.cards", [self.email]) 112 112 "noreply@flashy.cards", [self.email])
113 113
def notify(self): 114 114 def notify(self):
if self.registration_id is None: 115 115 if self.registration_id is None:
raise ValueError("user's registration_id is null") 116 116 raise ValueError("user's registration_id is null")
gcm_ctx = gcm.GCM(GCM_API_KEY) 117 117 gcm_ctx = gcm.GCM(GCM_API_KEY)
try: 118 118 try:
gcm_ctx.plaintext_request( 119 119 gcm_ctx.plaintext_request(
registration_id=self.registration_id, 120 120 registration_id=self.registration_id,
data={'poop': 'data'} 121 121 data={'poop': 'data'}
) 122 122 )
except (gcm.GCMInvalidRegistrationException, gcm.GCMMissingRegistrationException): 123 123 except (gcm.GCMInvalidRegistrationException, gcm.GCMMissingRegistrationException):
self.registration_id = None 124 124 self.registration_id = None
self.last_notified = now() 125 125 self.last_notified = now()
self.save() 126 126 self.save()
127 127
def set_registration_id(self, token): 128 128 def set_registration_id(self, token):
self.registration_id = token 129 129 self.registration_id = token
self.save() 130 130 self.save()
131 131
def is_in_section(self, section): 132 132 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 133 133 return self.sections.filter(pk=section.pk).exists()
134 134
def pull(self, flashcard): 135 135 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 136 136 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 137 137 raise ValueError("User not in the section this flashcard belongs to")
138 138
try: 139 139 try:
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 140 140 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
except IntegrityError: 141 141 except IntegrityError:
raise FlashcardAlreadyPulledException() 142 142 raise FlashcardAlreadyPulledException()
143 143
flashcard.refresh_score() 144 144 flashcard.refresh_score()
145 145
import flashcards.pushes 146 146 import flashcards.pushes
147 147
flashcards.pushes.push_feed_event('score_change', flashcard) 148 148 flashcards.pushes.push_feed_event('score_change', flashcard)
flashcards.pushes.push_deck_event('card_pulled', flashcard, self) 149 149 flashcards.pushes.push_deck_event('card_pulled', flashcard, self)
150 150
return user_card 151 151 return user_card
152 152
def unpull(self, flashcard): 153 153 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 154 154 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 155 155 raise ValueError("User not in the section this flashcard belongs to")
try: 156 156 try:
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 157 157 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
except UserFlashcard.DoesNotExist: 158 158 except UserFlashcard.DoesNotExist:
raise FlashcardNotInDeckException() 159 159 raise FlashcardNotInDeckException()
user_card.delete() 160 160 user_card.delete()
161 161
flashcard.refresh_score() 162 162 flashcard.refresh_score()
163 163
import flashcards.pushes 164 164 import flashcards.pushes
165 165
flashcards.pushes.push_feed_event('score_change', flashcard) 166 166 flashcards.pushes.push_feed_event('score_change', flashcard)
flashcards.pushes.push_deck_event('card_unpulled', flashcard, self) 167 167 flashcards.pushes.push_deck_event('card_unpulled', flashcard, self)
168 168
def get_deck(self, section): 169 169 def get_deck(self, section):
if not self.is_in_section(section): 170 170 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 171 171 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 172 172 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
173 173
def request_password_reset(self): 174 174 def request_password_reset(self):
token = default_token_generator.make_token(self) 175 175 token = default_token_generator.make_token(self)
176 176
body = ''' 177 177 body = '''
Visit the following link to reset your password: 178 178 Visit the following link to reset your password:
%sapp/resetpassword/%d/%s 179 179 %sapp/resetpassword/%d/%s
180 180
If you did not request a password reset, no action is required. 181 181 If you did not request a password reset, no action is required.
''' 182 182 '''
183 183
send_mail("Flashy password reset", 184 184 send_mail("Flashy password reset",
body % (ABSOLUTE_URL_ROOT, self.pk, token), 185 185 body % (ABSOLUTE_URL_ROOT, self.pk, token),
"noreply@flashy.cards", 186 186 "noreply@flashy.cards",
[self.email]) 187 187 [self.email])
188 188
@classmethod 189 189 @classmethod
def confirm_email(cls, confirmation_key): 190 190 def confirm_email(cls, confirmation_key):
# This will raise an exception if the email address is invalid 191 191 # This will raise an exception if the email address is invalid
address = EmailAddress.objects.confirm(confirmation_key, save=True).email 192 192 address = EmailAddress.objects.confirm(confirmation_key, save=True).email
user = cls.objects.get(email=address) 193 193 user = cls.objects.get(email=address)
user.confirmed_email = True 194 194 user.confirmed_email = True
user.save() 195 195 user.save()
return address 196 196 return address
197 197
def by_retention(self, sections, material_date_begin, material_date_end): 198 198 def by_retention(self, sections, material_date_begin, material_date_end):
user_flashcard_filter = UserFlashcard.objects.filter( 199 199 user_flashcard_filter = UserFlashcard.objects.filter(
user=self, flashcard__section__pk__in=sections, 200 200 user=self, flashcard__section__pk__in=sections,
flashcard__material_date__gte=material_date_begin, 201 201 flashcard__material_date__gte=material_date_begin,
flashcard__material_date__lte=material_date_end 202 202 flashcard__material_date__lte=material_date_end
) 203 203 )
204 204
if not user_flashcard_filter.exists(): 205 205 if not user_flashcard_filter.exists():
raise ValidationError("No matching flashcard found in your decks") 206 206 raise ValidationError("No matching flashcard found in your decks")
207 207
return user_flashcard_filter.order_by('next_review') 208 208 return user_flashcard_filter.order_by('next_review')
209 209
210 210
class UserFlashcard(Model): 211 211 class UserFlashcard(Model):
""" 212 212 """
Represents the relationship between a user and a flashcard by: 213 213 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 214 214 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 215 215 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 216 216 3. A user has a flashcard hidden from them
""" 217 217 """
user = ForeignKey('User') 218 218 user = ForeignKey('User')
mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card") 219 219 mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 220 220 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 221 221 flashcard = ForeignKey('Flashcard')
next_review = DateTimeField(null=True) 222 222 next_review = DateTimeField(null=True)
last_interval = IntegerField(default=1) 223 223 last_interval = IntegerField(default=1)
last_response_factor = FloatField(default=2.5) 224 224 last_response_factor = FloatField(default=2.5)
225 225
q_dict = {(False, False): 0, (False, True): 1, (False, None): 2, 226 226 q_dict = {(False, False): 0, (False, True): 1, (False, None): 2,
(True, False): 3, (True, None): 4, (True, True): 5} 227 227 (True, False): 3, (True, None): 4, (True, True): 5}
228 228
def get_mask(self): 229 229 def get_mask(self):
if self.mask is None: 230 230 if self.mask is None:
return self.flashcard.mask 231 231 return self.flashcard.mask
return self.mask 232 232 return self.mask
233 233
def save(self, force_insert=False, force_update=False, using=None, 234 234 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 235 235 update_fields=None):
if self.pk is None: 236 236 if self.pk is None:
self.next_review = now() + timedelta(days=1) 237 237 self.next_review = now() + timedelta(days=1)
super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update, 238 238 super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 239 239 using=using, update_fields=update_fields)
240 240
def review(self, user_flashcard_quiz): 241 241 def review(self, user_flashcard_quiz):
q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct] 242 242 q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct]
if self.last_interval == 1: 243 243 if self.last_interval == 1:
self.last_interval = 6 244 244 self.last_interval = 6
else: 245 245 else:
self.last_response_factor = max(1.3, self.last_response_factor + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02))) 246 246 self.last_response_factor = max(1.3, self.last_response_factor + (0.1 - (5 - q) * (0.08 + (5 - q) * 0.02)))
# self.last_interval = max(1, self.last_interval + (now() - self.next_review).days) 247 247 # self.last_interval = max(1, self.last_interval + (now() - self.next_review).days)
self.last_interval = int(round(self.last_interval * self.last_response_factor)) 248 248 self.last_interval = int(round(self.last_interval * self.last_response_factor))
self.next_review = now() + timedelta(days=self.last_interval) 249 249 self.next_review = now() + timedelta(days=self.last_interval)
self.save() 250 250 self.save()
251 251
class Meta: 252 252 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 253 253 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 254 254 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 255 255 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 256 256 # By default, order by most recently pulled
ordering = ['-pulled'] 257 257 ordering = ['-pulled']
258 258
def __unicode__(self): 259 259 def __unicode__(self):
return '%s has %s' % (str(self.user), str(self.flashcard)) 260 260 return '%s has %s' % (str(self.user), str(self.flashcard))
261 261
262 262
class FlashcardHide(Model): 263 263 class FlashcardHide(Model):
""" 264 264 """
Represents the property of a flashcard being hidden by a user. 265 265 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 266 266 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 267 267 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. 268 268 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 269 269 """
user = ForeignKey('User') 270 270 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 271 271 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 272 272 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 273 273 hidden = DateTimeField(auto_now_add=True)
274 274
class Meta: 275 275 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 276 276 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 277 277 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 278 278 index_together = ["user", "flashcard"]
279 279
def __unicode__(self): 280 280 def __unicode__(self):
return u'%s hid %s' % (unicode(self.user), unicode(self.flashcard)) 281 281 return u'%s hid %s' % (unicode(self.user), unicode(self.flashcard))
282 282
283 283
class Flashcard(Model): 284 284 class Flashcard(Model):
text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)]) 285 285 text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)])
section = ForeignKey('Section', help_text='The section with which the card is associated') 286 286 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") 287 287 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") 288 288 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 289 289 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 290 290 help_text="The previous version of this card, if one exists")
score = FloatField(default=0) 291 291 score = FloatField(default=0)
author = ForeignKey(User) 292 292 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 293 293 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 294 294 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 295 295 help_text="Reason for hiding this card")
mask = MaskField(null=True, blank=True, help_text="The mask on the card") 296 296 mask = MaskField(null=True, blank=True, help_text="The mask on the card")
297 297
class Meta: 298 298 class Meta:
# By default, order by most recently pushed 299 299 # By default, order by most recently pushed
ordering = ['-pushed'] 300 300 ordering = ['-pushed']
301 301
def __unicode__(self): 302 302 def __unicode__(self):
return u'<flashcard: %s>' % self.text 303 303 return u'<flashcard: %s>' % self.text
304 304
def refresh_score(self): 305 305 def refresh_score(self):
self.score = self.calculate_score 306 306 self.score = self.calculate_score
self.save() 307 307 self.save()
308 308
@classmethod 309 309 @classmethod
def push(cls, **kwargs): 310 310 def push(cls, **kwargs):
card = cls(**kwargs) 311 311 card = cls(**kwargs)
card.save() 312 312 card.save()
card.author.pull(card) 313 313 card.author.pull(card)
import flashcards.pushes 314 314 import flashcards.pushes
315 315
flashcards.pushes.push_feed_event('new_card', card) 316 316 flashcards.pushes.push_feed_event('new_card', card)
317 317
return card 318 318 return card
319 319
@property 320 320 @property
def material_week_num(self): 321 321 def material_week_num(self):
return (self.material_date - QUARTER_START).days / 7 + 1 322 322 return (self.material_date - QUARTER_START).days / 7 + 1
323 323
def is_hidden_from(self, user): 324 324 def is_hidden_from(self, user):
""" 325 325 """
A card can be hidden globally, but if a user has the card in their deck, 326 326 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 327 327 this visibility overrides a global hide.
:param user: 328 328 :param user:
:return: Whether the card is hidden from the user. 329 329 :return: Whether the card is hidden from the user.
""" 330 330 """
# if hasattr(self, 'is_not_hidden') and self.is_not_hidden: return False 331 331 # if hasattr(self, 'is_not_hidden') and self.is_not_hidden: return False
return self.is_hidden or self.flashcardhide_set.filter(user=user).exists() 332 332 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
333 333
def is_in_deck(self, user): 334 334 def is_in_deck(self, user):
if hasattr(self, 'userflashcard_id'): return self.userflashcard_id 335 335 if hasattr(self, 'userflashcard_id'): return self.userflashcard_id
return self.userflashcard_set.filter(user=user).exists() 336 336 return self.userflashcard_set.filter(user=user).exists()
337 337
def edit(self, user, new_data): 338 338 def edit(self, user, new_data):
""" 339 339 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 340 340 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. 341 341 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 342 342 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 343 343 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 344 344 """
material_date_changed = ('material_date' in new_data and self.material_date != new_data['material_date']) 345 345 material_date_changed = ('material_date' in new_data and self.material_date != new_data['material_date'])
text_changed = ('text' in new_data and self.text != new_data['text']) 346 346 text_changed = ('text' in new_data and self.text != new_data['text'])
mask_changed = ('mask' in new_data and self.mask != new_data['mask']) 347 347 mask_changed = ('mask' in new_data and self.mask != new_data['mask'])
new_mask = FlashcardMask(new_data['mask']) if mask_changed else self.mask 348 348 new_mask = FlashcardMask(new_data['mask']) if mask_changed else self.mask
content_changed = text_changed or material_date_changed 349 349 content_changed = text_changed or material_date_changed
create_new = (text_changed or material_date_changed) and ( 350 350 create_new = (text_changed or material_date_changed) and (
user != self.author or UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()) 351 351 user != self.author or UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists())
if create_new: 352 352 if create_new:
self.hide_by_user(user, 'replaced by new card') 353 353 self.hide_by_user(user, 'replaced by new card')
Flashcard.push(text=new_data.get('text', self.text), 354 354 return Flashcard.push(text=new_data.get('text', self.text),
mask=new_mask, 355 355 mask=new_mask,
material_date=new_data.get('material_date', self.material_date), 356 356 material_date=new_data.get('material_date', self.material_date),
section=self.section, 357 357 section=self.section,
previous=self, 358 358 previous=self,
author=user) 359 359 author=user)
else: 360 360 else:
self.mask = FlashcardMask(new_data['mask']) 361 361 self.mask = new_mask
if text_changed: self.text = new_data['text'] 362 362 if text_changed: self.text = new_data['text']
if material_date_changed: self.material_date = new_data['material_date'] 363 363 if material_date_changed: self.material_date = new_data['material_date']
self.save() 364 364 self.save()
if not self.is_in_deck(user): user.pull(self) 365 365 if not self.is_in_deck(user): user.pull(self)
if not content_changed: 366 366 if not content_changed:
user_card = UserFlashcard.objects.get(user=user, flashcard=self) 367 367 user_card = UserFlashcard.objects.get(user=user, flashcard=self)
user_card.mask = new_mask 368 368 user_card.mask = new_mask
user_card.save() 369 369 user_card.save()
370 370
import flashcards.pushes 371 371 import flashcards.pushes
372 372
flashcards.pushes.push_deck_event('card_fixed', self, user) 373 373 flashcards.pushes.push_deck_event('card_fixed', self, user)
return self 374 374 return self
375 375
def hide_by_user(self, user, reason=None): 376 376 def hide_by_user(self, user, reason=None):
377 377
if self.is_in_deck(user): user.unpull(self) 378 378 if self.is_in_deck(user): user.unpull(self)
hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 379 379 hide, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
hide.reason = reason 380 380 hide.reason = reason
hide.save() 381 381 hide.save()
if not created: return 382 382 if not created: return
import flashcards.pushes 383 383 import flashcards.pushes
384 384
flashcards.pushes.push_deck_event('card_hidden', self, user) 385 385 flashcards.pushes.push_deck_event('card_hidden', self, user)
386 386
def unhide_by_user(self, user, reason=None): 387 387 def unhide_by_user(self, user, reason=None):
import flashcards.pushes 388 388 import flashcards.pushes
389 389
flashcards.pushes.push_deck_event('card_unhidden', self, user) 390 390 flashcards.pushes.push_deck_event('card_unhidden', self, user)
hide = self.flashcardhide_set.get(user=user) 391 391 hide = self.flashcardhide_set.get(user=user)
hide.delete() 392 392 hide.delete()
393 393
@cached_property 394 394 @cached_property
def calculate_score(self): 395 395 def calculate_score(self):
def seconds_since_epoch(dt): 396 396 def seconds_since_epoch(dt):
from datetime import datetime 397 397 from datetime import datetime
398 398
epoch = make_aware(datetime.utcfromtimestamp(0)) 399 399 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 400 400 delta = dt - epoch
return delta.total_seconds() 401 401 return delta.total_seconds()
402 402
z = 0 403 403 z = 0
rate = 1.0 / 120 404 404 rate = 1.0 / 120
for vote in self.userflashcard_set.iterator(): 405 405 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 406 406 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 407 407 u = max(z, rate * t)
v = min(z, rate * t) 408 408 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 409 409 z = u + log1p(exp(v - u))
return z 410 410 return z
411 411
@classmethod 412 412 @classmethod
def cards_visible_to(cls, user): 413 413 def cards_visible_to(cls, user):
""" 414 414 """
:param user: 415 415 :param user:
:return: A queryset with all cards that should be visible to a user. 416 416 :return: A queryset with all cards that should be visible to a user.
""" 417 417 """
# All flashcards where the author is either confirmed, or the user 418 418 # All flashcards where the author is either confirmed, or the user
rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)) 419 419 rqs = cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user))
# Exclude hidden cards 420 420 # Exclude hidden cards
rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 421 421 rqs = rqs.exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
# rqs = rqs.prefetch_related('userflashcard_set') 422 422 # rqs = rqs.prefetch_related('userflashcard_set')
# rqs = rqs.aggregate(Count(userflashcard__user=user)) 423 423 # rqs = rqs.aggregate(Count(userflashcard__user=user))
# Annotate the cards so we don't have to check if they're hidden in the future 424 424 # Annotate the cards so we don't have to check if they're hidden in the future
return rqs.annotate(is_not_hidden=Value(True, output_field=BooleanField())) 425 425 return rqs.annotate(is_not_hidden=Value(True, output_field=BooleanField()))
426 426
@classmethod 427 427 @classmethod
def cards_hidden_by(cls, user): 428 428 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 429 429 return cls.objects.filter(flashcardhide__user=user)
430 430
431 431
class UserFlashcardQuiz(Model): 432 432 class UserFlashcardQuiz(Model):
""" 433 433 """
An event of a user being quizzed on a flashcard. 434 434 An event of a user being quizzed on a flashcard.
""" 435 435 """
user_flashcard = ForeignKey(UserFlashcard) 436 436 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 437 437 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=255, blank=True, help_text="The character range which was blanked") 438 438 blanked_word = CharField(max_length=255, 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") 439 439 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") 440 440 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
441 441
def __unicode__(self): 442 442 def __unicode__(self):
return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard)) 443 443 return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard))
444 444
def save(self, force_insert=False, force_update=False, using=None, 445 445 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 446 446 update_fields=None):
super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update, 447 447 super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 448 448 using=using, update_fields=update_fields)
self.user_flashcard.review(self) 449 449 self.user_flashcard.review(self)
450 450
def status(self): 451 451 def status(self):
""" 452 452 """
There are three stages of a quiz object: 453 453 There are three stages of a quiz object:
1. the user has been shown the card 454 454 1. the user has been shown the card
2. the user has answered the card 455 455 2. the user has answered the card
3. the user has self-evaluated their response's correctness 456 456 3. the user has self-evaluated their response's correctness
457 457
:return: string (evaluated, answered, viewed) 458 458 :return: string (evaluated, answered, viewed)
""" 459 459 """
if self.correct is not None: return "evaluated" 460 460 if self.correct is not None: return "evaluated"
if self.response: return "answered" 461 461 if self.response: return "answered"
return "viewed" 462 462 return "viewed"
463 463
464 464
class Section(Model): 465 465 class Section(Model):
""" 466 466 """
A UCSD course taught by an instructor during a quarter. 467 467 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 468 468 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 469 469 We index gratuitously to support autofill and because this is primarily read-only
""" 470 470 """
department = CharField(db_index=True, max_length=50) 471 471 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 472 472 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 473 473 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 474 474 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 475 475 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 476 476 quarter = CharField(db_index=True, max_length=4)
PAGE_SIZE = 40 477 477 PAGE_SIZE = 40
478 478
@classmethod 479 479 @classmethod
def search(cls, terms): 480 480 def search(cls, terms):
""" 481 481 """
Search all fields of all sections for a particular set of terms 482 482 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 483 483 A matching section must match at least one field on each term
:param terms:iterable 484 484 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 485 485 :return: Matching QuerySet ordered by department and course number
""" 486 486 """
final_q = Q() 487 487 final_q = Q()
for term in terms: 488 488 for term in terms:
q = Q(department__icontains=term) 489 489 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 490 490 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 491 491 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 492 492 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 493 493 q |= Q(instructor__icontains=term)
final_q &= q 494 494 final_q &= q
qs = cls.objects.filter(final_q).prefetch_related('whitelist') 495 495 qs = cls.objects.filter(final_q).prefetch_related('whitelist')
# Have the database cast the course number to an integer so it will sort properly 496 496 # 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 497 497 # 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)"}) 498 498 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 499 499 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 500 500 return qs
501 501
@property 502 502 @property
def is_whitelisted(self): 503 503 def is_whitelisted(self):
""" 504 504 """
:return: whether a whitelist exists for this section 505 505 :return: whether a whitelist exists for this section
""" 506 506 """
data = cache.get("section_%d_is_whitelisted" % self.pk) 507 507 data = cache.get("section_%d_is_whitelisted" % self.pk)
if data is None: 508 508 if data is None:
data = self.whitelist.exists() 509 509 data = self.whitelist.exists()
flashcards/tests/test_models.py View file @ f75aee4
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard, UserFlashcard, UserFlashcardQuiz 4 4 from flashcards.models import User, Section, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer 6 6 from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer
from flashy.settings import QUARTER_START, QUARTER_END 7 7 from flashy.settings import QUARTER_START, QUARTER_END
8 8
9 9
class RegistrationTests(TestCase): 10 10 class RegistrationTests(TestCase):
def setUp(self): 11 11 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 12 12 User.objects.create_user(email="none@none.com", password="1234")
13 13
def test_email_confirmation(self): 14 14 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 15 15 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 16 16 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 17 17 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 18 18 self.assertTrue(user.is_confirmed)
19 19
20 20
class UserTests(TestCase): 21 21 class UserTests(TestCase):
def setUp(self): 22 22 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 23 23 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 24 24 Section.objects.create(department='dept',
course_num='101a', 25 25 course_num='101a',
course_title='how 2 test', 26 26 course_title='how 2 test',
instructor='George Lucas', 27 27 instructor='George Lucas',
quarter='SP15') 28 28 quarter='SP15')
29 29
def test_section_list(self): 30 30 def test_section_list(self):
section = Section.objects.get(course_num='101a') 31 31 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 32 32 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 33 33 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 34 34 user.sections.add(section)
self.assertIn(section, user.sections.all()) 35 35 self.assertIn(section, user.sections.all())
user.sections.add(section) 36 36 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 37 37 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 38 38 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 39 39 self.assertEqual(user.sections.count(), 0)
40 40
41 41
class FlashcardMaskTest(TestCase): 42 42 class FlashcardMaskTest(TestCase):
def test_empty(self): 43 43 def test_empty(self):
try: 44 44 try:
fm = FlashcardMask([]) 45 45 fm = FlashcardMask([])
self.assertEqual(fm.max_offset(), -1) 46 46 self.assertEqual(fm.max_offset(), -1)
except TypeError: 47 47 except TypeError:
self.fail() 48 48 self.fail()
try: 49 49 try:
fm = FlashcardMask('') 50 50 fm = FlashcardMask('')
self.assertEqual(fm.max_offset(), -1) 51 51 self.assertEqual(fm.max_offset(), -1)
except TypeError: 52 52 except TypeError:
self.fail() 53 53 self.fail()
try: 54 54 try:
fm = FlashcardMask(None) 55 55 fm = FlashcardMask(None)
self.assertEqual(fm.max_offset(), -1) 56 56 self.assertEqual(fm.max_offset(), -1)
except TypeError: 57 57 except TypeError:
self.fail() 58 58 self.fail()
59 59
def test_iterable(self): 60 60 def test_iterable(self):
try: 61 61 try:
FlashcardMask(1) 62 62 FlashcardMask(1)
except TypeError as te: 63 63 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 64 64 self.assertEqual(te.message, "Interval not a valid iterable")
try: 65 65 try:
FlashcardMask([1, 2, 4]) 66 66 FlashcardMask([1, 2, 4])
except TypeError as te: 67 67 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 68 68 self.assertEqual(te.message, "Interval not a valid iterable")
69 69
def test_interval(self): 70 70 def test_interval(self):
try: 71 71 try:
FlashcardMask([[1, 2, 3], [1]]) 72 72 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 73 73 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 74 74 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 75 75 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 76 76 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 77 77 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 78 78 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 79 79 try:
FlashcardMask(([1, 2], [1])) 80 80 FlashcardMask(([1, 2], [1]))
except TypeError as te: 81 81 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 82 82 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 83 83 try:
FlashcardMask("[1,2,3]") 84 84 FlashcardMask("[1,2,3]")
except TypeError as te: 85 85 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 86 86 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
87 87
def test_overlap(self): 88 88 def test_overlap(self):
try: 89 89 try:
FlashcardMask({(1, 2), (2, 5)}) 90 90 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 91 91 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 92 92 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 93 93 try:
FlashcardMask({(1, 20), (12, 15)}) 94 94 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 95 95 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 96 96 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 97 97 try:
FlashcardMask({(2, 1), (5, 2)}) 98 98 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 99 99 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 100 100 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
101 101
102 102
class FlashcardTests(TestCase): 103 103 class FlashcardTests(TestCase):
def setUp(self): 104 104 def setUp(self):
section = Section.objects.create(department='dept', 105 105 section = Section.objects.create(department='dept',
course_num='101a', 106 106 course_num='101a',
course_title='how 2 test', 107 107 course_title='how 2 test',
instructor='George Lucas', 108 108 instructor='George Lucas',
quarter='SP15') 109 109 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 110 110 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 111 111 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 112 112 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 113 113 section=section,
author=user, 114 114 author=user,
material_date=datetime.now(), 115 115 material_date=datetime.now(),
previous=None, 116 116 previous=None,
mask={(24, 34), (0, 4)}) 117 117 mask={(24, 34), (0, 4)})
user.save() 118 118 user.save()
section.save() 119 119 section.save()
flashcard.save() 120 120 flashcard.save()
121 121
def test_flashcard_edit(self): 122 122 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 123 123 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 124 124 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 125 125 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 126 126 user2.sections.add(section)
user2.save() 127 127 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 128 128 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 129 129 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 130 130 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 131 131 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 132 132 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 133 133 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 134 134 flashcard = flashcard.edit(user2, {'text': 'This is the new text'})
135
self.assertNotEqual(flashcard.pk, pk_backup) 135 136 self.assertNotEqual(flashcard.pk, pk_backup)
self.assertEqual(flashcard.text, 'This is the new text') 136 137 self.assertEqual(flashcard.text, 'This is the new text')
137 138
def test_mask_field(self): 138 139 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 139 140 user = User.objects.get(email="none@none.com")
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 140 141 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 141 142 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 142 143 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 143 144 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 144 145 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 145 146 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
try: 146 147 try:
flashcard.mask = {(10, 34), (0, 14)} 147 148 flashcard.mask = {(10, 34), (0, 14)}
flashcard.save() 148 149 flashcard.save()
self.fail() 149 150 self.fail()
except OverlapIntervalException: 150 151 except OverlapIntervalException:
self.assertTrue(True) 151 152 self.assertTrue(True)
152 153
153 154
class UserFlashcardQuizTests(TestCase): 154 155 class UserFlashcardQuizTests(TestCase):
def setUp(self): 155 156 def setUp(self):
self.section = Section.objects.create(department='dept', 156 157 self.section = Section.objects.create(department='dept',
course_num='101a', 157 158 course_num='101a',
course_title='how 2 test', 158 159 course_title='how 2 test',
instructor='George Lucas', 159 160 instructor='George Lucas',
quarter='SP15') 160 161 quarter='SP15')
self.user = User.objects.create_user(email="none@none.com", password="1234") 161 162 self.user = User.objects.create_user(email="none@none.com", password="1234")
self.user.sections.add(self.section) 162 163 self.user.sections.add(self.section)
self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 163 164 self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=self.section, 164 165 section=self.section,
author=self.user, 165 166 author=self.user,
material_date=datetime.now(), 166 167 material_date=datetime.now(),
previous=None, 167 168 previous=None,
mask=[(24, 33), (0, 4)]) 168 169 mask=[(24, 33), (0, 4)])
self.user.save() 169 170 self.user.save()
self.section.save() 170 171 self.section.save()
self.flashcard.save() 171 172 self.flashcard.save()
self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard, 172 173 self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard,
user=self.user, 173 174 user=self.user,
mask=self.flashcard.mask, 174 175 mask=self.flashcard.mask,
pulled=datetime.now()) 175 176 pulled=datetime.now())
self.user_flashcard.save() 176 177 self.user_flashcard.save()
self.user_flashcard.refresh_from_db() 177 178 self.user_flashcard.refresh_from_db()
self.flashcard.refresh_from_db() 178 179 self.flashcard.refresh_from_db()
179 180
def test_quiz_request(self): 180 181 def test_quiz_request(self):
data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END} 181 182 data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END}
serializer = QuizRequestSerializer(data=data) 182 183 serializer = QuizRequestSerializer(data=data)
serializer.is_valid(raise_exception=True) 183 184 serializer.is_valid(raise_exception=True)
validated_data = serializer.create(serializer.validated_data) 184 185 validated_data = serializer.create(serializer.validated_data)
user_flashcard = UserFlashcard.objects.filter(user=self.user, 185 186 user_flashcard = UserFlashcard.objects.filter(user=self.user,
flashcard__section__pk__in=validated_data['sections'], 186 187 flashcard__section__pk__in=validated_data['sections'],
flashcard__material_date__gte=validated_data['material_date_begin'], 187 188 flashcard__material_date__gte=validated_data['material_date_begin'],
flashcard__material_date__lte=validated_data['material_date_end']) 188 189 flashcard__material_date__lte=validated_data['material_date_end'])
self.assertTrue(user_flashcard.exists()) 189 190 self.assertTrue(user_flashcard.exists())
190 191
user_flashcard = user_flashcard.first() 191 192 user_flashcard = user_flashcard.first()
self.assertEqual(user_flashcard, self.user_flashcard) 192 193 self.assertEqual(user_flashcard, self.user_flashcard)
193 194
mask = user_flashcard.mask.get_random_blank() 194 195 mask = user_flashcard.mask.get_random_blank()
word = user_flashcard.flashcard.text[slice(*mask)] 195 196 word = user_flashcard.flashcard.text[slice(*mask)]
user_flashcard_quiz = UserFlashcardQuiz(user_flashcard=user_flashcard, 196 197 user_flashcard_quiz = UserFlashcardQuiz(user_flashcard=user_flashcard,
blanked_word=word) 197 198 blanked_word=word)
user_flashcard_quiz.save() 198 199 user_flashcard_quiz.save()
self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz)) 199 200 self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz))
self.assertIn(mask, [[24, 33], [0, 4]]) 200 201 self.assertIn(mask, [[24, 33], [0, 4]])
201 202
user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)] 202 203 user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)]
self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"]) 203 204 self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"])