Commit cec534fd3cccbe0358af9217e29855043407f167

Authored by Andrew Buss
1 parent 2958a1827e
Exists in master

moved more view logic into the model layer

Showing 3 changed files with 12 additions and 16 deletions Inline Diff

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