Commit 776577266a0d1426343db9613ab50e013e0daee1

Authored by Andrew Buss
1 parent 6713d6dc9f
Exists in master

websockets notifications on new cards and score updates

Showing 4 changed files with 40 additions and 12 deletions Inline Diff

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