Commit cc2ad68a38a3d45676ed92a20ded5a3ad54b782e

Authored by Andrew Buss
Exists in master

Merge branch 'master' of git.ucsd.edu:110swag/flashy-backend

Showing 3 changed files Inline Diff

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