Commit 83e4896b059edd0c668a1a0cb7dc2eca023125c5

Authored by Andrew Buss
1 parent 1eebdbcc41
Exists in master

include is_enrolled in sectionserializer

Showing 2 changed files with 12 additions and 3 deletions Inline Diff

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