Commit 5a2899b9ddc175cef2e7d334d462f7d7f7e30ee6

Authored by Andrew Buss
1 parent 33f8a47a8b
Exists in master

add gobs of logging

Showing 4 changed files with 69 additions and 10 deletions Inline Diff

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