Commit 148b7e62d21d370011b87b073ee6e6ba30d55349

Authored by Rohan Rangray
1 parent ddba4aeba1
Exists in master

Attempted fix for /deck/ notifications.

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

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