Commit 7a17fac38ec351f93f4a1144abeefedd92874620

Authored by Andrew Buss
1 parent 01eeffc6fd
Exists in master

add view to resend confirmation email

Showing 3 changed files with 21 additions and 11 deletions Inline Diff

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