Commit ef51cef6a0ca832baa1d49e5e8a34b6c0eb99155

Authored by Andrew Buss
1 parent 5c40ced6df
Exists in master

log a user in after resetting their password

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

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