Commit ee4104aa2338e1dcdf6a2063a521aad923318262

Authored by Rohan Rangray
1 parent 8e66c8186f
Exists in master

Added a Study viewset and the associated serializers.

Showing 4 changed files with 176 additions and 21 deletions Inline Diff

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