Commit 2f415807e1163b7e38ab262502e23820b82aed39

Authored by Rohan Rangray
Exists in master

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

mering'

Showing 4 changed files Inline Diff

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