Commit 90df9a55c8b4b5dcd989a8f4520aa7526354091e

Authored by Andrew Buss
1 parent 5c1348fee8
Exists in master

django-debug-toolbar; performance improvements

Showing 3 changed files with 15 additions and 3 deletions Inline Diff

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