Commit 4e105f55eb02fea1995cfe830b7abb8d7913f837

Authored by Andrew Buss
1 parent 5458c8b297
Exists in master

allow a user to hide a card twice - the second reason overwrites the original

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

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