Commit 750af8a0f32f39ba3bccba940fb5f40162cde986

Authored by Andrew Buss
1 parent 6c31b00541
Exists in master

add unpull event

Showing 2 changed files with 17 additions and 15 deletions Inline Diff

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