Commit 22f26c6fac44e554120980224f1a004560cb1f4a

Authored by Andrew Buss
1 parent a3f17ed32e
Exists in master

hide the old card when we edit

Showing 1 changed file with 3 additions and 0 deletions Inline Diff

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