Commit fe546f43f2c7c9a1f7fafbaae9f59147d4506f79

Authored by Andrew Buss
1 parent 7a17fac38e
Exists in master

move verify_email outside user update

Showing 5 changed files with 33 additions and 24 deletions Inline Diff

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