Commit 000b5980c9b7ffcd1901d7b4701e553236e5a2b6

Authored by Rohan Rangray
Exists in master

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

ermging

Showing 4 changed files Inline Diff

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