Commit d818aecbc5ea37e00222ddb9b34930dba4870439

Authored by Rohan Rangray
Exists in master

Merged.

Showing 7 changed files Inline Diff

flashcards/api.py View file @ d818aec
from flashcards.models import Flashcard 1 1 from flashcards.models import Flashcard, UserFlashcardQuiz
from rest_framework.pagination import PageNumberPagination 2 2 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 3 3 from rest_framework.permissions import BasePermission
4 4
5 5
mock_no_params = lambda x:None 6 6 mock_no_params = lambda x:None
7 7
class StandardResultsSetPagination(PageNumberPagination): 8 8 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 9 9 page_size = 40
page_size_query_param = 'page_size' 10 10 page_size_query_param = 'page_size'
max_page_size = 1000 11 11 max_page_size = 1000
12 12
13 13
class UserDetailPermissions(BasePermission): 14 14 class UserDetailPermissions(BasePermission):
""" 15 15 """
Permissions for the user detail view. Anonymous users may only POST. 16 16 Permissions for the user detail view. Anonymous users may only POST.
""" 17 17 """
18 18
def has_object_permission(self, request, view, obj): 19 19 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 20 20 if request.method == 'POST':
return True 21 21 return True
return request.user.is_authenticated() 22 22 return request.user.is_authenticated()
23 23
24 24
class IsEnrolledInAssociatedSection(BasePermission): 25 25 class IsEnrolledInAssociatedSection(BasePermission):
def has_object_permission(self, request, view, obj): 26 26 def has_object_permission(self, request, view, obj):
27 if obj is None:
28 return True
assert type(obj) is Flashcard 27 29 assert type(obj) is Flashcard
return request.user.is_in_section(obj.section) 28 30 return request.user.is_in_section(obj.section)
31
flashcards/models.py View file @ d818aec
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 2 2 from django.contrib.auth.tokens import default_token_generator
3 from django.core.cache import cache
from django.core.exceptions import ValidationError 3 4 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 4 5 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 5 6 from django.core.mail import send_mail
from django.db import IntegrityError 6 7 from django.db import IntegrityError
from django.db.models import * 7 8 from django.db.models import *
from django.utils.timezone import now 8 9 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 9 10 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 10 11 from fields import MaskField
11 12
12 13
# Hack to fix AbstractUser before subclassing it 13 14 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 14 15 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 15 16 AbstractUser._meta.get_field('username')._unique = False
16 17
17 18
class EmailOnlyUserManager(UserManager): 18 19 class EmailOnlyUserManager(UserManager):
""" 19 20 """
A tiny extension of Django's UserManager which correctly creates users 20 21 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 21 22 without usernames (using emails instead).
""" 22 23 """
23 24
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 24 25 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 25 26 """
Creates and saves a User with the given email and password. 26 27 Creates and saves a User with the given email and password.
""" 27 28 """
email = self.normalize_email(email) 28 29 email = self.normalize_email(email)
user = self.model(email=email, 29 30 user = self.model(email=email,
is_staff=is_staff, is_active=True, 30 31 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 31 32 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 32 33 date_joined=now(), **extra_fields)
user.set_password(password) 33 34 user.set_password(password)
user.save(using=self._db) 34 35 user.save(using=self._db)
return user 35 36 return user
36 37
def create_user(self, email, password=None, **extra_fields): 37 38 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 38 39 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 39 40 body = '''
Visit the following link to confirm your email address: 40 41 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 41 42 https://flashy.cards/app/verifyemail/%s
42 43
If you did not register for Flashy, no action is required. 43 44 If you did not register for Flashy, no action is required.
''' 44 45 '''
45 46
assert send_mail("Flashy email verification", 46 47 assert send_mail("Flashy email verification",
body % user.confirmation_key, 47 48 body % user.confirmation_key,
"noreply@flashy.cards", 48 49 "noreply@flashy.cards",
[user.email]) 49 50 [user.email])
return user 50 51 return user
51 52
def create_superuser(self, email, password, **extra_fields): 52 53 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 53 54 return self._create_user(email, password, True, True, **extra_fields)
54 55
55 56
56
57
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 58 57 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 59 58 """
An extension of Django's default user model. 60 59 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 61 60 We use email as the username field, and include enrolled sections here
""" 62 61 """
objects = EmailOnlyUserManager() 63 62 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 64 63 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 65 64 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 66 65 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
67 66
def is_in_section(self, section): 68 67 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 69 68 return self.sections.filter(pk=section.pk).exists()
70 69
def pull(self, flashcard): 71 70 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 72 71 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 73 72 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 74 73 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 75 74 user_card.pulled = now()
user_card.save() 76 75 user_card.save()
77 76
def unpull(self, flashcard): 78 77 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 79 78 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 80 79 raise ValueError("User not in the section this flashcard belongs to")
81 80
try: 82 81 try:
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 83 82 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
except UserFlashcard.DoesNotExist: 84 83 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 85 84 raise ValueError('Cannot unpull card that is not pulled.')
86 85
user_card.delete() 87 86 user_card.delete()
88 87
def get_deck(self, section): 89 88 def get_deck(self, section):
if not self.is_in_section(section): 90 89 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 91 90 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 92 91 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
93 92
def request_password_reset(self): 94 93 def request_password_reset(self):
token = default_token_generator.make_token(self) 95 94 token = default_token_generator.make_token(self)
96 95
body = ''' 97 96 body = '''
Visit the following link to reset your password: 98 97 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 99 98 https://flashy.cards/app/resetpassword/%d/%s
100 99
If you did not request a password reset, no action is required. 101 100 If you did not request a password reset, no action is required.
''' 102 101 '''
103 102
send_mail("Flashy password reset", 104 103 send_mail("Flashy password reset",
body % (self.pk, token), 105 104 body % (self.pk, token),
"noreply@flashy.cards", 106 105 "noreply@flashy.cards",
[self.email]) 107 106 [self.email])
108 107
109 108
class UserFlashcard(Model): 110 109 class UserFlashcard(Model):
""" 111 110 """
Represents the relationship between a user and a flashcard by: 112 111 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 113 112 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 114 113 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 115 114 3. A user has a flashcard hidden from them
""" 116 115 """
user = ForeignKey('User') 117 116 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 118 117 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 119 118 help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") 120 119 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 121 120 flashcard = ForeignKey('Flashcard')
122 121
class Meta: 123 122 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 124 123 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 125 124 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 126 125 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 127 126 # By default, order by most recently pulled
ordering = ['-pulled'] 128 127 ordering = ['-pulled']
129 128
130 129
class FlashcardHide(Model): 131 130 class FlashcardHide(Model):
""" 132 131 """
Represents the property of a flashcard being hidden by a user. 133 132 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 134 133 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 135 134 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. 136 135 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 137 136 """
user = ForeignKey('User') 138 137 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 139 138 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 140 139 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 141 140 hidden = DateTimeField(auto_now_add=True)
142 141
class Meta: 143 142 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 144 143 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 145 144 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 146 145 index_together = ["user", "flashcard"]
147 146
148 147
class Flashcard(Model): 149 148 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 150 149 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 151 150 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") 152 151 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") 153 152 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 154 153 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 155 154 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 156 155 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 157 156 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 158 157 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 159 158 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 160 159 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
161 160
class Meta: 162 161 class Meta:
# By default, order by most recently pushed 163 162 # By default, order by most recently pushed
ordering = ['-pushed'] 164 163 ordering = ['-pushed']
165 164
def is_hidden_from(self, user): 166 165 def is_hidden_from(self, user):
""" 167 166 """
A card can be hidden globally, but if a user has the card in their deck, 168 167 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 169 168 this visibility overrides a global hide.
:param user: 170 169 :param user:
:return: Whether the card is hidden from the user. 171 170 :return: Whether the card is hidden from the user.
""" 172 171 """
if self.userflashcard_set.filter(user=user).exists(): return False 173 172 if self.userflashcard_set.filter(user=user).exists(): return False
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 174 173 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return False 175 174 return False
176 175
def hide_from(self, user, reason=None): 177 176 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 178 177 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 179 178 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 180 179 if not created:
raise ValidationError("The card has already been hidden.") 181 180 raise ValidationError("The card has already been hidden.")
obj.save() 182 181 obj.save()
183 182
def is_in_deck(self, user): 184 183 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 185 184 return self.userflashcard_set.filter(user=user).exists()
186 185
def add_to_deck(self, user): 187 186 def add_to_deck(self, user):
if not user.is_in_section(self.section): 188 187 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 189 188 raise PermissionDenied("You don't have the permission to add this card")
try: 190 189 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 191 190 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 192 191 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 193 192 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 194 193 user_flashcard.save()
return user_flashcard 195 194 return user_flashcard
196 195
def edit(self, user, new_data): 197 196 def edit(self, user, new_data):
""" 198 197 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 199 198 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. 200 199 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 201 200 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 202 201 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 203 202 """
204 203
# content_changed is True iff either material_date or text were changed 205 204 # content_changed is True iff either material_date or text were changed
content_changed = False 206 205 content_changed = False
# create_new is True iff the user editing this card is the author of this card 207 206 # 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 208 207 # and there are no other users with this card in their decks
create_new = user != self.author or \ 209 208 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 210 209 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 211 210 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 212 211 content_changed = True
self.material_date = new_data['material_date'] 213 212 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 214 213 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 215 214 content_changed = True
self.text = new_data['text'] 216 215 self.text = new_data['text']
if create_new and content_changed: 217 216 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 218 217 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 219 218 self.previous_id = self.pk
self.pk = None 220 219 self.pk = None
self.mask = new_data.get('mask', self.mask) 221 220 self.mask = new_data.get('mask', self.mask)
self.save() 222 221 self.save()
self.add_to_deck(user) 223 222 self.add_to_deck(user)
else: 224 223 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 225 224 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 226 225 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 227 226 user_card.save()
return self 228 227 return self
229 228
def report(self, user, reason=None): 230 229 def report(self, user, reason=None):
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 231 230 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 232 231 obj.reason = reason
obj.save() 233 232 obj.save()
234 233
@classmethod 235 234 @classmethod
def cards_visible_to(cls, user): 236 235 def cards_visible_to(cls, user):
""" 237 236 """
:param user: 238 237 :param user:
:return: A queryset with all cards that should be visible to a user. 239 238 :return: A queryset with all cards that should be visible to a user.
""" 240 239 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 241 240 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
242 241
243 242
class UserFlashcardQuiz(Model): 244 243 class UserFlashcardQuiz(Model):
""" 245 244 """
An event of a user being quizzed on a flashcard. 246 245 An event of a user being quizzed on a flashcard.
""" 247 246 """
user_flashcard = ForeignKey(UserFlashcard) 248 247 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 249 248 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 250 249 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") 251 250 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") 252 251 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
253 252
def status(self): 254 253 def status(self):
""" 255 254 """
There are three stages of a quiz object: 256 255 There are three stages of a quiz object:
1. the user has been shown the card 257 256 1. the user has been shown the card
2. the user has answered the card 258 257 2. the user has answered the card
3. the user has self-evaluated their response's correctness 259 258 3. the user has self-evaluated their response's correctness
260 259
:return: string (evaluated, answered, viewed) 261 260 :return: string (evaluated, answered, viewed)
""" 262 261 """
if self.correct is not None: return "evaluated" 263 262 if self.correct is not None: return "evaluated"
if self.response: return "answered" 264 263 if self.response: return "answered"
return "viewed" 265 264 return "viewed"
266 265
267 266
class Section(Model): 268 267 class Section(Model):
""" 269 268 """
A UCSD course taught by an instructor during a quarter. 270 269 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 271 270 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 272 271 We index gratuitously to support autofill and because this is primarily read-only
""" 273 272 """
department = CharField(db_index=True, max_length=50) 274 273 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 275 274 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 276 275 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 277 276 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 278 277 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 279 278 quarter = CharField(db_index=True, max_length=4)
280 279
@classmethod 281 280 @classmethod
def search(cls, terms): 282 281 def search(cls, terms):
""" 283 282 """
Search all fields of all sections for a particular set of terms 284 283 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 285 284 A matching section must match at least one field on each term
:param terms:iterable 286 285 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 287 286 :return: Matching QuerySet ordered by department and course number
""" 288 287 """
final_q = Q() 289 288 final_q = Q()
for term in terms: 290 289 for term in terms:
q = Q(department__icontains=term) 291 290 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 292 291 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 293 292 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 294 293 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 295 294 q |= Q(instructor__icontains=term)
final_q &= q 296 295 final_q &= q
qs = cls.objects.filter(final_q) 297 296 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 298 297 # 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 299 298 # 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)"}) 300 299 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 301 300 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 302 301 return qs
303 302
@property 304 303 @property
def is_whitelisted(self): 305 304 def is_whitelisted(self):
""" 306 305 """
:return: whether a whitelist exists for this section 307 306 :return: whether a whitelist exists for this section
""" 308 307 """
return self.whitelist.exists() 309 308 return self.whitelist.exists()
310 309
def is_user_on_whitelist(self, user): 311 310 def is_user_on_whitelist(self, user):
""" 312 311 """
:return: whether the user is on the waitlist for this section 313 312 :return: whether the user is on the waitlist for this section
""" 314 313 """
return self.whitelist.filter(email=user.email).exists() 315 314 return self.whitelist.filter(email=user.email).exists()
316 315
317 316
def enroll(self, user): 318 317 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 319 318 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 320 319 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 321 320 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 322 321 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 323 322 self.user_set.add(user)
324 323
def drop(self, user): 325 324 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 326 325 if not user.sections.filter(pk=self.pk).exists():
raise ValidationError("User is not enrolled in the section.") 327 326 raise ValidationError("User is not enrolled in the section.")
self.user_set.remove(user) 328 327 self.user_set.remove(user)
329 328
class Meta: 330 329 class Meta:
ordering = ['-course_title'] 331 330 ordering = ['department_abbreviation', 'course_num']
332 331
@property 333 332 @property
flashcards/serializers.py View file @ d818aec
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
import pytz 5 5 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz 6 6 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 7 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 8 8 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 10 10 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 11 11 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 12 12 from flashy.settings import QUARTER_END, QUARTER_START
from random import sample 13 13 from random import sample
14 14
15 15
class EmailSerializer(Serializer): 16 16 class EmailSerializer(Serializer):
email = EmailField(required=True) 17 17 email = EmailField(required=True)
18 18
19 19
class EmailPasswordSerializer(EmailSerializer): 20 20 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 21 21 password = CharField(required=True)
22 22
23 23
class RegistrationSerializer(EmailPasswordSerializer): 24 24 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 25 25 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
26 26
27 27
class PasswordResetRequestSerializer(EmailSerializer): 28 28 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 29 29 def validate_email(self, value):
try: 30 30 try:
User.objects.get(email=value) 31 31 User.objects.get(email=value)
return value 32 32 return value
except User.DoesNotExist: 33 33 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 34 34 raise serializers.ValidationError('No user exists with that email')
35 35
36 36
class PasswordResetSerializer(Serializer): 37 37 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 38 38 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 39 39 uid = IntegerField(required=True)
token = CharField(required=True) 40 40 token = CharField(required=True)
41 41
def validate_uid(self, value): 42 42 def validate_uid(self, value):
try: 43 43 try:
User.objects.get(id=value) 44 44 User.objects.get(id=value)
return value 45 45 return value
except User.DoesNotExist: 46 46 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 47 47 raise serializers.ValidationError('Could not verify reset token')
48 48
49 49
class UserUpdateSerializer(Serializer): 50 50 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 51 51 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 52 52 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 53 53 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 54 54 # reset_token = CharField(required=False)
55 55
def validate(self, data): 56 56 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 57 57 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 58 58 raise serializers.ValidationError('old_password is required to set a new_password')
return data 59 59 return data
60 60
61 61
class Password(Serializer): 62 62 class Password(Serializer):
email = EmailField(required=True) 63 63 email = EmailField(required=True)
password = CharField(required=True) 64 64 password = CharField(required=True)
65 65
66 66
class LecturePeriodSerializer(ModelSerializer): 67 67 class LecturePeriodSerializer(ModelSerializer):
class Meta: 68 68 class Meta:
model = LecturePeriod 69 69 model = LecturePeriod
exclude = 'id', 'section' 70 70 exclude = 'id', 'section'
71 71
72 72
class SectionSerializer(ModelSerializer): 73 73 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 74
lecture_times = CharField() 75 74 lecture_times = CharField()
short_name = CharField() 76 75 short_name = CharField()
long_name = CharField() 77 76 long_name = CharField()
78 77
class Meta: 79 78 class Meta:
model = Section 80 79 model = Section
80
81 class DeepSectionSerializer(SectionSerializer):
82 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
83
81 84
82 85
class UserSerializer(ModelSerializer): 83 86 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 84 87 email = EmailField(required=False)
sections = SectionSerializer(many=True) 85 88 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 86 89 is_confirmed = BooleanField()
87 90
class Meta: 88 91 class Meta:
model = User 89 92 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 90 93 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
91 94
92 95
class MaskFieldSerializer(serializers.Field): 93 96 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 94 97 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 95 98 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 96 99 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 97 100 'overlap': 'Ensure this field does not have overlapping intervals.'
} 98 101 }
99 102
def to_representation(self, value): 100 103 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 101 104 return dumps(list(self._make_mask(value)))
102 105
def to_internal_value(self, value): 103 106 def to_internal_value(self, value):
return self._make_mask(loads(value)) 104 107 return self._make_mask(loads(value))
105 108
def _make_mask(self, data): 106 109 def _make_mask(self, data):
try: 107 110 try:
mask = FlashcardMask(data) 108 111 mask = FlashcardMask(data)
except ValueError: 109 112 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 110 113 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 111 114 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 112 115 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 113 116 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 114 117 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 115 118 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 116 119 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 117 120 return mask
118 121
119 122
class FlashcardSerializer(ModelSerializer): 120 123 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 121 124 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 122 125 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 123 126 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 124 127 mask = MaskFieldSerializer(allow_null=True)
125 128
def validate_material_date(self, value): 126 129 def validate_material_date(self, value):
# TODO: make this dynamic 127 130 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 128 131 if QUARTER_START <= value <= QUARTER_END:
return value 129 132 return value
else: 130 133 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 131 134 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
132 135
def validate_pushed(self, value): 133 136 def validate_pushed(self, value):
if value > datetime.now(): 134 137 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 135 138 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 136 139 return value
137 140
def validate_mask(self, value): 138 141 def validate_mask(self, value):
if value is None: 139 142 if value is None:
return None 140 143 return None
if len(self.initial_data['text']) < value.max_offset(): 141 144 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 142 145 raise serializers.ValidationError("Mask out of bounds")
return value 143 146 return value
144 147
class Meta: 145 148 class Meta:
model = Flashcard 146 149 model = Flashcard
exclude = 'author', 'previous' 147 150 exclude = 'author', 'previous'
148 151
149 152
class FlashcardUpdateSerializer(serializers.Serializer): 150 153 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 151 154 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 152 155 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 153 156 mask = MaskFieldSerializer(required=False)
154 157
def validate_material_date(self, date): 155 158 def validate_material_date(self, date):
if date > QUARTER_END: 156 159 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 157 160 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 158 161 return date
159 162
def validate(self, attrs): 160 163 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 161 164 # Make sure that at least one of the attributes was passed in
if not any(i in attrs for i in ['material_date', 'text', 'mask']): 162 165 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 163 166 raise serializers.ValidationError("No new value passed in")
return attrs 164 167 return attrs
165 168
166 169
class QuizRequestSerializer(serializers.Serializer): 167 170 class QuizRequestSerializer(serializers.Serializer):
sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True) 168 171 sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True)
material_date_begin = DateTimeField(default=QUARTER_START) 169 172 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 170 173 material_date_end = DateTimeField(default=QUARTER_END)
171 174
def __init__(self, user, *args, **kwargs): 172 175 def __init__(self, user, *args, **kwargs):
super(QuizRequestSerializer, self).__init__(*args, **kwargs) 173 176 super(QuizRequestSerializer, self).__init__(*args, **kwargs)
self.user = user 174 177 self.user = user
self.user_flashcard = None 175 178 self.user_flashcard = None
176 179
def create(self, validated_data): 177 180 def create(self, validated_data):
return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard) 178 181 return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard)
179 182
def update(self, instance, validated_data): 180 183 def update(self, instance, validated_data):
for attr in validated_data: 181 184 for attr in validated_data:
setattr(instance, attr, validated_data[attr]) 182 185 setattr(instance, attr, validated_data[attr])
instance.save() 183 186 instance.save()
return instance 184 187 return instance
185 188
def _get_user_flashcard(self, attrs): 186 189 def _get_user_flashcard(self, attrs):
user_flashcard_filter = UserFlashcard.objects.filter( 187 190 user_flashcard_filter = UserFlashcard.objects.filter(
user=self.user, flashcard__section__in=attrs['sections'], 188 191 user=self.user, flashcard__section__in=attrs['sections'],
flashcard__material_date__gte=attrs['material_date_begin'], 189 192 flashcard__material_date__gte=attrs['material_date_begin'],
flashcard__material_date__lte=attrs['material_date_end'] 190 193 flashcard__material_date__lte=attrs['material_date_end']
) 191 194 )
if not user_flashcard_filter.exists(): 192 195 if not user_flashcard_filter.exists():
raise serializers.ValidationError("Your deck for that section is empty") 193 196 raise serializers.ValidationError("Your deck for that section is empty")
self.user_flashcard = user_flashcard_filter.order_by('?').first() 194 197 self.user_flashcard = user_flashcard_filter.order_by('?').first()
195 198
def validate_material_date_begin(self, value): 196 199 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 197 200 if QUARTER_START <= value <= QUARTER_END:
return value 198 201 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 199 202 raise serializers.ValidationError("Invalid begin date for the flashcard range")
200 203
def validate_material_date_end(self, value): 201 204 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 202 205 if QUARTER_START <= value <= QUARTER_END:
return value 203 206 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 204 207 raise serializers.ValidationError("Invalid end date for the flashcard range")
205 208
def validate_sections(self, value): 206 209 def validate_sections(self, value):
print "VALUE", type(value), value 207 210 print "VALUE", type(value), value
if value is None: 208 211 if value is None:
return self.user.sections 209 212 return self.user.sections
section_filter = Section.objects.filter(pk__in=value) 210 213 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 211 214 if not section_filter.exists():
raise serializers.ValidationError("You aren't enrolled in those section(s)") 212 215 raise serializers.ValidationError("You aren't enrolled in those section(s)")
return section_filter 213 216 return section_filter
214 217
def validate(self, attrs): 215 218 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 216 219 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 217 220 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 218 221 if 'sections' not in attrs:
attrs['sections'] = self.validate_sections(None) 219 222 attrs['sections'] = self.validate_sections(None)
self._get_user_flashcard(attrs) 220 223 self._get_user_flashcard(attrs)
flashcards/validators.py View file @ d818aec
from collections import Iterable 1 1 from collections import Iterable
2 from random import sample
2 3
3 4
class FlashcardMask(set): 4 5 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 5 6 def __init__(self, iterable, *args, **kwargs):
if iterable is None or iterable == '': 6 7 if iterable is None or iterable == '':
iterable = [] 7 8 iterable = []
self._iterable_check(iterable) 8 9 self._iterable_check(iterable)
iterable = map(tuple, iterable) 9 10 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 10 11 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 11 12 self._interval_check()
self._overlap_check() 12 13 self._overlap_check()
13 14
def max_offset(self): 14 15 def max_offset(self):
return self._end 15 16 return self._end
17
18 def get_random_blank(self):
19 if self.max_offset() > 0:
20 return sample(self, 1)
21 return ()
16 22
def _iterable_check(self, iterable): 17 23 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 18 24 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 19 25 raise TypeError("Interval not a valid iterable")
20 26
def _interval_check(self): 21 27 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 22 28 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 23 29 raise TypeError("Intervals must have exactly 2 elements, begin and end")
24 30
def _overlap_check(self): 25 31 def _overlap_check(self):
p_beg, p_end = -1, -1 26 32 p_beg, p_end = -1, -1
for interval in sorted(self): 27 33 for interval in sorted(self):
beg, end = map(int, interval) 28 34 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 29 35 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") 30 36 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 31 37 p_beg, p_end = beg, end
self._end = p_end 32 38 self._end = p_end
flashcards/views.py View file @ d818aec
import django 1 1 import django
2 2
from django.contrib import auth 3 3 from django.contrib import auth
4 from django.core.cache import cache
from django.shortcuts import get_object_or_404 4 5 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 5 6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz 6 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 7 8 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 8 9 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer 9 10 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
11 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 10 12 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 11 13 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 12 14 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 13 15 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 14 16 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 15 17 from django.core.mail import send_mail
from django.contrib.auth import authenticate 16 18 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 17 19 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 18 20 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 19 21 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 20 22 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 21 23 from simple_email_confirmation import EmailAddress
from random import sample 22 24 from random import sample
23 25
24 26
class SectionViewSet(ReadOnlyModelViewSet): 25 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 26 28 queryset = Section.objects.all()
serializer_class = SectionSerializer 27 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 28 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 29 31 permission_classes = [IsAuthenticated]
30 32
@detail_route(methods=['GET']) 31 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 32 34 def flashcards(self, request, pk):
""" 33 35 """
Gets flashcards for a section, excluding hidden cards. 34 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 35 37 Returned in strictly chronological order (material date).
""" 36 38 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 37 39 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 38 40 return Response(FlashcardSerializer(flashcards, many=True).data)
39 41
@detail_route(methods=['POST']) 40 42 @detail_route(methods=['POST'])
def enroll(self, request, pk): 41 43 def enroll(self, request, pk):
""" 42 44 """
Add the current user to a specified section 43 45 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. 44 46 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 45 47 ---
view_mocker: flashcards.api.mock_no_params 46 48 view_mocker: flashcards.api.mock_no_params
""" 47 49 """
48 50
self.get_object().enroll(request.user) 49 51 self.get_object().enroll(request.user)
return Response(status=HTTP_204_NO_CONTENT) 50 52 return Response(status=HTTP_204_NO_CONTENT)
51 53
@detail_route(methods=['POST']) 52 54 @detail_route(methods=['POST'])
def drop(self, request, pk): 53 55 def drop(self, request, pk):
""" 54 56 """
Remove the current user from a specified section 55 57 Remove the current user from a specified section
If the user is not in the class, the request will fail. 56 58 If the user is not in the class, the request will fail.
--- 57 59 ---
view_mocker: flashcards.api.mock_no_params 58 60 view_mocker: flashcards.api.mock_no_params
""" 59 61 """
try: 60 62 try:
self.get_object().drop(request.user) 61 63 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 62 64 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 63 65 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 64 66 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 65 67 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 66 68 return Response(status=HTTP_204_NO_CONTENT)
67 69
@list_route(methods=['GET']) 68 70 @list_route(methods=['GET'])
def search(self, request): 69 71 def search(self, request):
""" 70 72 """
Returns a list of sections which match a user's query 71 73 Returns a list of sections which match a user's query
--- 72 74 ---
parameters: 73 75 parameters:
- name: q 74 76 - name: q
description: space-separated list of terms 75 77 description: space-separated list of terms
required: true 76 78 required: true
type: form 77 79 type: form
80 response_serializer: SectionSerializer
""" 78 81 """
query = request.GET.get('q', None) 79 82 query = request.GET.get('q', None)
if not query: return Response('[]') 80 83 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 81 84 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 82 85 data = SectionSerializer(qs, many=True).data
return Response(serializer.data) 83 86 return Response(data)
84 87
@detail_route(methods=['GET']) 85 88 @detail_route(methods=['GET'])
def deck(self, request, pk): 86 89 def deck(self, request, pk):
""" 87 90 """
Gets the contents of a user's deck for a given section. 88 91 Gets the contents of a user's deck for a given section.
""" 89 92 """
qs = request.user.get_deck(self.get_object()) 90 93 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 91 94 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 92 95 return Response(serializer.data)
93 96
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 94 97 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 95 98 def ordered_deck(self, request, pk):
""" 96 99 """
Get a chronological order by material_date of flashcards for a section. 97 100 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 98 101 This excludes hidden card.
""" 99 102 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 100 103 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 101 104 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 102 105 return Response(serializer.data)
103 106
@detail_route(methods=['GET']) 104 107 @detail_route(methods=['GET'])
def feed(self, request, pk): 105 108 def feed(self, request, pk):
""" 106 109 """
Gets the contents of a user's feed for a section. 107 110 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 108 111 Exclude cards that are already in the user's deck
""" 109 112 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 110 113 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 111 114 return Response(serializer.data)
112 115
113 116
class UserSectionListView(ListAPIView): 114 117 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 115 118 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 116 119 permission_classes = [IsAuthenticated]
117 120
def get_queryset(self): 118 121 def get_queryset(self):
return self.request.user.sections.all() 119 122 return self.request.user.sections.all()
120 123
def paginate_queryset(self, queryset): return None 121 124 def paginate_queryset(self, queryset): return None
122 125
123 126
class UserDetail(GenericAPIView): 124 127 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 125 128 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 126 129 permission_classes = [IsAuthenticated]
127 130
def patch(self, request, format=None): 128 131 def patch(self, request, format=None):
""" 129 132 """
Updates the user's password, or verifies their email address 130 133 Updates the user's password, or verifies their email address
--- 131 134 ---
request_serializer: UserUpdateSerializer 132 135 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 133 136 response_serializer: UserSerializer
""" 134 137 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 135 138 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 136 139 data.is_valid(raise_exception=True)
data = data.validated_data 137 140 data = data.validated_data
138 141
if 'new_password' in data: 139 142 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 140 143 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 141 144 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 142 145 request.user.set_password(data['new_password'])
request.user.save() 143 146 request.user.save()
144 147
if 'confirmation_key' in data: 145 148 if 'confirmation_key' in data:
try: 146 149 try:
request.user.confirm_email(data['confirmation_key']) 147 150 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 148 151 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 149 152 raise ValidationError('confirmation_key is invalid')
150 153
return Response(UserSerializer(request.user).data) 151 154 return Response(UserSerializer(request.user).data)
152 155
def get(self, request, format=None): 153 156 def get(self, request, format=None):
""" 154 157 """
Return data about the user 155 158 Return data about the user
--- 156 159 ---
response_serializer: UserSerializer 157 160 response_serializer: UserSerializer
""" 158 161 """
serializer = UserSerializer(request.user, context={'request': request}) 159 162 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 160 163 return Response(serializer.data)
161 164
def delete(self, request): 162 165 def delete(self, request):
""" 163 166 """
Irrevocably delete the user and their data 164 167 Irrevocably delete the user and their data
165 168
Yes, really 166 169 Yes, really
""" 167 170 """
request.user.delete() 168 171 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 169 172 return Response(status=HTTP_204_NO_CONTENT)
170 173
171 174
@api_view(['POST']) 172 175 @api_view(['POST'])
def register(request, format=None): 173 176 def register(request, format=None):
""" 174 177 """
Register a new user 175 178 Register a new user
--- 176 179 ---
request_serializer: EmailPasswordSerializer 177 180 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 178 181 response_serializer: UserSerializer
""" 179 182 """
data = RegistrationSerializer(data=request.data) 180 183 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 181 184 data.is_valid(raise_exception=True)
182 185
User.objects.create_user(**data.validated_data) 183 186 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 184 187 user = authenticate(**data.validated_data)
auth.login(request, user) 185 188 auth.login(request, user)
186 189
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 187 190 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
188 191
189 192
@api_view(['POST']) 190 193 @api_view(['POST'])
def login(request): 191 194 def login(request):
""" 192 195 """
Authenticates user and returns user data if valid. 193 196 Authenticates user and returns user data if valid.
--- 194 197 ---
request_serializer: EmailPasswordSerializer 195 198 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 196 199 response_serializer: UserSerializer
""" 197 200 """
198 201
data = EmailPasswordSerializer(data=request.data) 199 202 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 200 203 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 201 204 user = authenticate(**data.validated_data)
202 205
if user is None: 203 206 if user is None:
raise AuthenticationFailed('Invalid email or password') 204 207 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 205 208 if not user.is_active:
raise NotAuthenticated('Account is disabled') 206 209 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 207 210 auth.login(request, user)
return Response(UserSerializer(request.user).data) 208 211 return Response(UserSerializer(request.user).data)
209 212
210 213
@api_view(['POST']) 211 214 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 212 215 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 213 216 def logout(request, format=None):
""" 214 217 """
Logs the authenticated user out. 215 218 Logs the authenticated user out.
""" 216 219 """
auth.logout(request) 217 220 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 218 221 return Response(status=HTTP_204_NO_CONTENT)
219 222
220 223
@api_view(['POST']) 221 224 @api_view(['POST'])
def request_password_reset(request, format=None): 222 225 def request_password_reset(request, format=None):
""" 223 226 """
Send a password reset token/link to the provided email. 224 227 Send a password reset token/link to the provided email.
--- 225 228 ---
request_serializer: PasswordResetRequestSerializer 226 229 request_serializer: PasswordResetRequestSerializer
""" 227 230 """
data = PasswordResetRequestSerializer(data=request.data) 228 231 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 229 232 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 230 233 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 231 234 return Response(status=HTTP_204_NO_CONTENT)
232 235
233 236
@api_view(['POST']) 234 237 @api_view(['POST'])
def reset_password(request, format=None): 235 238 def reset_password(request, format=None):
""" 236 239 """
Updates user's password to new password if token is valid. 237 240 Updates user's password to new password if token is valid.
--- 238 241 ---
request_serializer: PasswordResetSerializer 239 242 request_serializer: PasswordResetSerializer
""" 240 243 """
data = PasswordResetSerializer(data=request.data) 241 244 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 242 245 data.is_valid(raise_exception=True)
243 246
user = User.objects.get(id=data['uid'].value) 244 247 user = User.objects.get(id=data['uid'].value)
# Check token validity. 245 248 # Check token validity.
246 249
if default_token_generator.check_token(user, data['token'].value): 247 250 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 248 251 user.set_password(data['new_password'].value)
user.save() 249 252 user.save()
else: 250 253 else:
raise ValidationError('Could not verify reset token') 251 254 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 252 255 return Response(status=HTTP_204_NO_CONTENT)
253 256
254 257
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 255 258 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 256 259 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 257 260 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 258 261 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
259 262
# Override create in CreateModelMixin 260 263 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 261 264 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 262 265 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 263 266 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 264 267 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 265 268 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 266 269 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 267 270 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 268 271 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 269 272 self.perform_create(flashcard)
headers = self.get_success_headers(data) 270 273 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard) 271 274 response_data = FlashcardSerializer(flashcard)
return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) 272 275 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
273 276
274 277
@detail_route(methods=['POST']) 275 278 @detail_route(methods=['POST'])
def unhide(self, request, pk): 276 279 def unhide(self, request, pk):
""" 277 280 """
Unhide the given card 278 281 Unhide the given card
--- 279 282 ---
view_mocker: flashcards.api.mock_no_params 280 283 view_mocker: flashcards.api.mock_no_params
""" 281 284 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 282 285 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 283 286 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 284 287 return Response(status=HTTP_204_NO_CONTENT)
285 288
@detail_route(methods=['POST']) 286 289 @detail_route(methods=['POST'])
def report(self, request, pk): 287 290 def report(self, request, pk):
""" 288 291 """
Hide the given card 289 292 Hide the given card
--- 290 293 ---
view_mocker: flashcards.api.mock_no_params 291 294 view_mocker: flashcards.api.mock_no_params
""" 292 295 """
self.get_object().report(request.user) 293 296 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 294 297 return Response(status=HTTP_204_NO_CONTENT)
295 298
hide = report 296 299 hide = report
297 300
@detail_route(methods=['POST']) 298 301 @detail_route(methods=['POST'])
def pull(self, request, pk): 299 302 def pull(self, request, pk):
""" 300 303 """
Pull a card from the live feed into the user's deck. 301 304 Pull a card from the live feed into the user's deck.
--- 302 305 ---
view_mocker: flashcards.api.mock_no_params 303 306 view_mocker: flashcards.api.mock_no_params
""" 304 307 """
user = request.user 305 308 user = request.user
flashcard = self.get_object() 306 309 flashcard = self.get_object()
user.pull(flashcard) 307 310 user.pull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 308 311 return Response(status=HTTP_204_NO_CONTENT)
309 312
@detail_route(methods=['POST']) 310 313 @detail_route(methods=['POST'])
def unpull(self, request, pk): 311 314 def unpull(self, request, pk):
""" 312 315 """
Unpull a card from the user's deck 313 316 Unpull a card from the user's deck
--- 314 317 ---
view_mocker: flashcards.api.mock_no_params 315 318 view_mocker: flashcards.api.mock_no_params
""" 316 319 """
user = request.user 317 320 user = request.user
flashcard = self.get_object() 318 321 flashcard = self.get_object()
user.unpull(flashcard) 319 322 user.unpull(flashcard)
flashy/settings.py View file @ d818aec
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 1 1 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os 2 2 import os
from datetime import datetime 3 3 from datetime import datetime
from pytz import UTC 4 4 from pytz import UTC
5 5
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 6 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 7
IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ 8 8 IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ
9 9
DEBUG = not IN_PRODUCTION 10 10 DEBUG = not IN_PRODUCTION
11 11
ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] 12 12 ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
13 13
AUTH_USER_MODEL = 'flashcards.User' 14 14 AUTH_USER_MODEL = 'flashcards.User'
REST_FRAMEWORK = { 15 15 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 16 16 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20 17 17 'PAGE_SIZE': 20
} 18 18 }
INSTALLED_APPS = [ 19 19 INSTALLED_APPS = [
'simple_email_confirmation', 20 20 'simple_email_confirmation',
'flashcards', 21 21 'flashcards',
'django.contrib.admin', 22 22 'django.contrib.admin',
'django.contrib.admindocs', 23 23 'django.contrib.admindocs',
'django.contrib.auth', 24 24 'django.contrib.auth',
'django.contrib.contenttypes', 25 25 'django.contrib.contenttypes',
'django.contrib.sessions', 26 26 'django.contrib.sessions',
'django.contrib.messages', 27 27 'django.contrib.messages',
'django.contrib.staticfiles', 28 28 'django.contrib.staticfiles',
29 29 'ws4redis',
'rest_framework_swagger', 30 30 'rest_framework_swagger',
'rest_framework', 31 31 'rest_framework',
] 32 32 ]
33 33
34 WEBSOCKET_URL = '/ws/'
35
36
MIDDLEWARE_CLASSES = ( 34 37 MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 35 38 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 36 39 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 37 40 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 38 41 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 39 42 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 40 43 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 41 44 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 42 45 'django.middleware.security.SecurityMiddleware',
) 43 46 )
44 47
ROOT_URLCONF = 'flashy.urls' 45 48 ROOT_URLCONF = 'flashy.urls'
46 49
AUTHENTICATION_BACKENDS = ( 47 50 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 48 51 'django.contrib.auth.backends.ModelBackend',
) 49 52 )
50 53
TEMPLATES = [ 51 54 TEMPLATES = [
{ 52 55 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 53 56 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 54 57 'DIRS': ['templates/'],
'APP_DIRS': True, 55 58 'APP_DIRS': True,
'OPTIONS': { 56 59 'OPTIONS': {
'context_processors': [ 57 60 'context_processors': [
'django.template.context_processors.debug', 58 61 'django.template.context_processors.debug',
'django.template.context_processors.request', 59 62 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 60 63 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 61 64 'django.contrib.messages.context_processors.messages',
65 'django.core.context_processors.static',
66 'ws4redis.context_processors.default',
], 62 67 ],
}, 63 68 },
}, 64 69 },
] 65 70 ]
66 71
WSGI_APPLICATION = 'flashy.wsgi.application' 67 72 WSGI_APPLICATION = 'ws4redis.django_runserver.application'
68 73
DATABASES = { 69 74 DATABASES = {
'default': { 70 75 'default': {
'ENGINE': 'django.db.backends.sqlite3', 71 76 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 72 77 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 73 78 }
} 74 79 }
75 80
if IN_PRODUCTION: 76 81 if IN_PRODUCTION:
DATABASES['default'] = { 77 82 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 78 83 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 79 84 'NAME': 'flashy',
'USER': 'flashy', 80 85 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 81 86 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 82 87 'HOST': 'localhost',
'PORT': '', 83 88 'PORT': '',
} 84 89 }
85 90
LANGUAGE_CODE = 'en-us' 86 91 LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles' 87 92 TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True 88 93 USE_I18N = True
USE_L10N = True 89 94 USE_L10N = True
USE_TZ = True 90 95 USE_TZ = True
91 96
QUARTER_START = UTC.localize(datetime(2015, 3, 30)) 92 97 QUARTER_START = UTC.localize(datetime(2015, 3, 30))
QUARTER_END = UTC.localize(datetime(2015, 6, 12)) 93 98 QUARTER_END = UTC.localize(datetime(2015, 6, 12))
94 99
STATIC_URL = '/static/' 95 100 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 96 101 STATIC_ROOT = 'static'
97 102
# Four settings just to be sure 98 103 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 99 104 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 100 105 EMAIL_HOST_USER = 'noreply@flashy.cards'
DEFAULT_FROM_EMAIL = 'noreply@flashy.cards' 101 106 DEFAULT_FROM_EMAIL = 'noreply@flashy.cards'
SERVER_EMAIL = 'noreply@flashy.cards' 102 107 SERVER_EMAIL = 'noreply@flashy.cards'
103 108
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 104 109 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
105 110
if IN_PRODUCTION: 106 111 if IN_PRODUCTION:
INSTALLED_APPS.append('django_ses') 107 112 INSTALLED_APPS.append('django_ses')
AWS_SES_REGION_NAME = 'us-west-2' 108 113 AWS_SES_REGION_NAME = 'us-west-2'
AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com' 109 114 AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com'
EMAIL_BACKEND = 'django_ses.SESBackend' 110 115 EMAIL_BACKEND = 'django_ses.SESBackend'
111 116
if IN_PRODUCTION: 112 117 if IN_PRODUCTION:
SESSION_COOKIE_SECURE = True 113 118 SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True 114 119 CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 115 120 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# are we secure yet? 116 121 # are we secure yet?
117 122
if IN_PRODUCTION: 118 123 if IN_PRODUCTION:
LOGGING = { 119 124 LOGGING = {
'version': 1, 120 125 'version': 1,
'disable_existing_loggers': False, 121 126 'disable_existing_loggers': False,
'handlers': { 122 127 'handlers': {
'file': { 123 128 'file': {
'level': 'DEBUG', 124 129 'level': 'DEBUG',
'class': 'logging.FileHandler', 125 130 'class': 'logging.FileHandler',
'filename': 'debug.log', 126 131 'filename': 'debug.log',
}, 127 132 },
requirements.txt View file @ d818aec
#beautifulsoup4 1 1 #beautifulsoup4
Django>=1.8 2 2 Django>=1.8
#django-websocket-redis==0.4.3 3 3 django-websocket-redis
#gevent==1.0.1 4 4 #gevent==1.0.1
#greenlet==0.4.5 5 5 #greenlet==0.4.5
#redis==2.10.3 6 6 #redis==2.10.3
six==1.9.0 7 7 six==1.9.0
djangorestframework 8 8 djangorestframework
docutils 9 9 docutils
django-simple-email-confirmation 10 10 django-simple-email-confirmation
coverage 11 11 coverage
django-rest-swagger 12 12 django-rest-swagger
pytz 13 13 pytz
14 14