Commit a3ae545efed1c233e89ff9d803a03c85ceee6ee2

Authored by Andrew Buss
1 parent 4263a9ac9e
Exists in master

send user's chosen mask

Showing 2 changed files with 43 additions and 29 deletions Inline Diff

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