Commit 4263a9ac9e621b81dd4ee53cb2e577b92bfb8a11

Authored by Andrew Buss
1 parent 51fcb6875f
Exists in master

send is_authored_by, send feed events when a card is edited

Showing 4 changed files with 21 additions and 6 deletions Inline Diff

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