Commit 804b11e2338a423462b84506fd8caae4a16e3057

Authored by Andrew Buss
Exists in master

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

Showing 2 changed files Inline Diff

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