Commit dc942208b989ddbe3368e02fa8dc331b8bcca639

Authored by Rohan Rangray
Exists in master

Fixed merge issue

Showing 4 changed files Inline Diff

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