Commit 05b5a910c5e1a63a46a9d09e4b9173a0a823f6c6

Authored by Andrew Buss
1 parent 1890b5a76b
Exists in master

include locked field in userserializer

Showing 3 changed files with 15 additions and 9 deletions Inline Diff

flashcards/api.py View file @ 05b5a91
from django.utils.timezone import now 1
from flashcards.models import Flashcard, UserFlashcardQuiz 2 1 from flashcards.models import Flashcard, UserFlashcardQuiz
from rest_framework.exceptions import PermissionDenied 3 2 from rest_framework.exceptions import PermissionDenied
from rest_framework.pagination import PageNumberPagination 4 3 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 5 4 from rest_framework.permissions import BasePermission
6 5
mock_no_params = lambda x: None 7 6 mock_no_params = lambda x: None
8 7
9 8
class StandardResultsSetPagination(PageNumberPagination): 10 9 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 11 10 page_size = 40
page_size_query_param = 'page_size' 12 11 page_size_query_param = 'page_size'
max_page_size = 1000 13 12 max_page_size = 1000
14 13
15 14
class UserDetailPermissions(BasePermission): 16 15 class UserDetailPermissions(BasePermission):
""" 17 16 """
Permissions for the user detail view. Anonymous users may only POST. 18 17 Permissions for the user detail view. Anonymous users may only POST.
""" 19 18 """
20 19
def has_object_permission(self, request, view, obj): 21 20 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 22 21 if request.method == 'POST':
return True 23 22 return True
return request.user.is_authenticated() 24 23 return request.user.is_authenticated()
25 24
26 25
class IsEnrolledInAssociatedSection(BasePermission): 27 26 class IsEnrolledInAssociatedSection(BasePermission):
def has_object_permission(self, request, view, obj): 28 27 def has_object_permission(self, request, view, obj):
if obj is None: 29 28 if obj is None:
return True 30 29 return True
assert type(obj) is Flashcard 31 30 assert type(obj) is Flashcard
return request.user.is_in_section(obj.section) 32 31 return request.user.is_in_section(obj.section)
33 32
34 33
class IsFlashcardReviewer(BasePermission): 35 34 class IsFlashcardReviewer(BasePermission):
def has_object_permission(self, request, view, obj): 36 35 def has_object_permission(self, request, view, obj):
if obj is None: 37 36 if obj is None:
return True 38 37 return True
assert type(obj) is UserFlashcardQuiz 39 38 assert type(obj) is UserFlashcardQuiz
return request.user == obj.user_flashcard.user 40 39 return request.user == obj.user_flashcard.user
41 40
42 41
flashcards/models.py View file @ 05b5a91
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
from math import e 3 3 from math import e
4 4
from django.contrib.auth.models import AbstractUser, UserManager 5 5 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 6 6 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 7 7 from django.core.cache import cache
from django.core.exceptions import ValidationError 8 8 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 9 9 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 10 10 from django.core.mail import send_mail
from django.core.validators import MinLengthValidator 11 11 from django.core.validators import MinLengthValidator
from django.db import IntegrityError 12 12 from django.db import IntegrityError
from django.db.models import * 13 13 from django.db.models import *
from django.utils.timezone import now, make_aware 14 14 from django.utils.timezone import now, make_aware
from flashy.settings import QUARTER_START 15 15 from flashy.settings import QUARTER_START
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 16 16 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 17 17 from fields import MaskField
from cached_property import cached_property 18 18 from cached_property import cached_property
from flashy.settings import IN_PRODUCTION 19 19 from flashy.settings import IN_PRODUCTION
20 20
21 21
22 22
23 23
24 24
25
# Hack to fix AbstractUser before subclassing it 25 26 # Hack to fix AbstractUser before subclassing it
26 27
AbstractUser._meta.get_field('email')._unique = True 27 28 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 28 29 AbstractUser._meta.get_field('username')._unique = False
29 30
30 31
class EmailOnlyUserManager(UserManager): 31 32 class EmailOnlyUserManager(UserManager):
""" 32 33 """
A tiny extension of Django's UserManager which correctly creates users 33 34 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 34 35 without usernames (using emails instead).
""" 35 36 """
36 37
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 37 38 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 38 39 """
Creates and saves a User with the given email and password. 39 40 Creates and saves a User with the given email and password.
""" 40 41 """
email = self.normalize_email(email) 41 42 email = self.normalize_email(email)
user = self.model(email=email, 42 43 user = self.model(email=email,
is_staff=is_staff, is_active=True, 43 44 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 44 45 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 45 46 date_joined=now(), **extra_fields)
user.set_password(password) 46 47 user.set_password(password)
user.save(using=self._db) 47 48 user.save(using=self._db)
user.send_confirmation_email() 48 49 user.send_confirmation_email()
return user 49 50 return user
50 51
def create_user(self, email, password=None, **extra_fields): 51 52 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 52 53 user = self._create_user(email, password, False, False, **extra_fields)
53 54
return user 54 55 return user
55 56
def create_superuser(self, email, password, **extra_fields): 56 57 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 57 58 return self._create_user(email, password, True, True, **extra_fields)
58 59
59 60
class FlashcardAlreadyPulledException(Exception): 60 61 class FlashcardAlreadyPulledException(Exception):
pass 61 62 pass
62 63
63 64
class FlashcardNotInDeckException(Exception): 64 65 class FlashcardNotInDeckException(Exception):
pass 65 66 pass
66 67
67 68
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 68 69 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 69 70 """
An extension of Django's default user model. 70 71 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 71 72 We use email as the username field, and include enrolled sections here
""" 72 73 """
objects = EmailOnlyUserManager() 73 74 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 74 75 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 75 76 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 76 77 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 77 78 confirmed_email = BooleanField(default=False)
78 79
80 @property
81 def locked(self):
82 if self.confirmed_email: return False
83 return (now() - self.date_joined).days > 0
84
def send_confirmation_email(self): 79 85 def send_confirmation_email(self):
body = ''' 80 86 body = '''
Visit the following link to confirm your email address: 81 87 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 82 88 https://flashy.cards/app/verifyemail/%s
83 89
If you did not register for Flashy, no action is required. 84 90 If you did not register for Flashy, no action is required.
''' 85 91 '''
send_mail("Flashy email verification", body % self.confirmation_key, "noreply@flashy.cards", [self.email]) 86 92 send_mail("Flashy email verification", body % self.confirmation_key, "noreply@flashy.cards", [self.email])
87 93
def is_in_section(self, section): 88 94 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 89 95 return self.sections.filter(pk=section.pk).exists()
90 96
def pull(self, flashcard): 91 97 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 92 98 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 93 99 raise ValueError("User not in the section this flashcard belongs to")
94 100
try: 95 101 try:
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 96 102 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
except IntegrityError: 97 103 except IntegrityError:
raise FlashcardAlreadyPulledException() 98 104 raise FlashcardAlreadyPulledException()
user_card.save() 99 105 user_card.save()
100 106
import flashcards.notifications 101 107 import flashcards.notifications
102 108
flashcards.notifications.notify_score_change(flashcard) 103 109 flashcards.notifications.notify_score_change(flashcard)
flashcards.notifications.notify_pull(flashcard) 104 110 flashcards.notifications.notify_pull(flashcard)
105 111
def unpull(self, flashcard): 106 112 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 107 113 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 108 114 raise ValueError("User not in the section this flashcard belongs to")
109 115
try: 110 116 try:
import flashcards.notifications 111 117 import flashcards.notifications
112 118
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 113 119 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 114 120 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 115 121 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 116 122 except UserFlashcard.DoesNotExist:
raise FlashcardNotInDeckException() 117 123 raise FlashcardNotInDeckException()
118 124
def get_deck(self, section): 119 125 def get_deck(self, section):
if not self.is_in_section(section): 120 126 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 121 127 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 122 128 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
123 129
def request_password_reset(self): 124 130 def request_password_reset(self):
token = default_token_generator.make_token(self) 125 131 token = default_token_generator.make_token(self)
126 132
body = ''' 127 133 body = '''
Visit the following link to reset your password: 128 134 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 129 135 https://flashy.cards/app/resetpassword/%d/%s
130 136
If you did not request a password reset, no action is required. 131 137 If you did not request a password reset, no action is required.
''' 132 138 '''
133 139
send_mail("Flashy password reset", 134 140 send_mail("Flashy password reset",
body % (self.pk, token), 135 141 body % (self.pk, token),
"noreply@flashy.cards", 136 142 "noreply@flashy.cards",
[self.email]) 137 143 [self.email])
138 144
def confirm_email(self, confirmation_key): 139 145 def confirm_email(self, confirmation_key):
# This will raise an exception if the email address is invalid 140 146 # This will raise an exception if the email address is invalid
self.email_address_set.confirm(confirmation_key, save=True) 141 147 self.email_address_set.confirm(confirmation_key, save=True)
self.confirmed_email = True 142 148 self.confirmed_email = True
self.save() 143 149 self.save()
144 150
def by_retention(self, sections, material_date_begin, material_date_end): 145 151 def by_retention(self, sections, material_date_begin, material_date_end):
section_pks = sections.values_list('pk') 146 152 section_pks = sections.values_list('pk')
user_flashcard_filter = UserFlashcard.objects.filter( 147 153 user_flashcard_filter = UserFlashcard.objects.filter(
user=self, flashcard__section__pk__in=section_pks, 148 154 user=self, flashcard__section__pk__in=section_pks,
flashcard__material_date__gte=material_date_begin, 149 155 flashcard__material_date__gte=material_date_begin,
flashcard__material_date__lte=material_date_end 150 156 flashcard__material_date__lte=material_date_end
) 151 157 )
152 158
if not user_flashcard_filter.exists(): 153 159 if not user_flashcard_filter.exists():
raise ValidationError("No matching flashcard found in your decks") 154 160 raise ValidationError("No matching flashcard found in your decks")
155 161
return user_flashcard_filter.prefetch_related('userflashcardquiz_set').annotate( 156 162 return user_flashcard_filter.prefetch_related('userflashcardquiz_set').annotate(
study_count=Count('pk'), 157 163 study_count=Count('pk'),
days_since=Case( 158 164 days_since=Case(
When(userflashcardquiz__when=None, then=interval_days(Now(), F('pulled'))), 159 165 When(userflashcardquiz__when=None, then=interval_days(Now(), F('pulled'))),
default=interval_days(Now(), Max('userflashcardquiz__when')), 160 166 default=interval_days(Now(), Max('userflashcardquiz__when')),
output_field=FloatField() 161 167 output_field=FloatField()
), 162 168 ),
retention_score=Case( 163 169 retention_score=Case(
default=Value(e, output_field=FloatField()) ** (F('days_since') * (-0.1 / (F('study_count') + 1))), 164 170 default=Value(e, output_field=FloatField()) ** (F('days_since') * (-0.1 / (F('study_count') + 1))),
output_field=FloatField() 165 171 output_field=FloatField()
) 166 172 )
).order_by('retention_score') 167 173 ).order_by('retention_score')
168 174
169 175
class UserFlashcard(Model): 170 176 class UserFlashcard(Model):
""" 171 177 """
Represents the relationship between a user and a flashcard by: 172 178 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 173 179 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 174 180 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 175 181 3. A user has a flashcard hidden from them
""" 176 182 """
user = ForeignKey('User') 177 183 user = ForeignKey('User')
mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card") 178 184 mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 179 185 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 180 186 flashcard = ForeignKey('Flashcard')
181 187
def get_mask(self): 182 188 def get_mask(self):
if self.mask is None: 183 189 if self.mask is None:
return self.flashcard.mask 184 190 return self.flashcard.mask
return self.mask 185 191 return self.mask
186 192
class Meta: 187 193 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 188 194 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 189 195 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 190 196 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 191 197 # By default, order by most recently pulled
ordering = ['-pulled'] 192 198 ordering = ['-pulled']
193 199
def __unicode__(self): 194 200 def __unicode__(self):
return '%s has %s' % (str(self.user), str(self.flashcard)) 195 201 return '%s has %s' % (str(self.user), str(self.flashcard))
196 202
197 203
class FlashcardHide(Model): 198 204 class FlashcardHide(Model):
""" 199 205 """
Represents the property of a flashcard being hidden by a user. 200 206 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 201 207 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 202 208 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. 203 209 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 204 210 """
user = ForeignKey('User') 205 211 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 206 212 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 207 213 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 208 214 hidden = DateTimeField(auto_now_add=True)
209 215
class Meta: 210 216 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 211 217 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 212 218 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 213 219 index_together = ["user", "flashcard"]
214 220
def __unicode__(self): 215 221 def __unicode__(self):
return '%s hid %s' % (str(self.user), str(self.flashcard)) 216 222 return '%s hid %s' % (str(self.user), str(self.flashcard))
217 223
218 224
class Flashcard(Model): 219 225 class Flashcard(Model):
text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)]) 220 226 text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)])
section = ForeignKey('Section', help_text='The section with which the card is associated') 221 227 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") 222 228 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") 223 229 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 224 230 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 225 231 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 226 232 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 227 233 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 228 234 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 229 235 help_text="Reason for hiding this card")
mask = MaskField(null=True, blank=True, help_text="The mask on the card") 230 236 mask = MaskField(null=True, blank=True, help_text="The mask on the card")
231 237
class Meta: 232 238 class Meta:
# By default, order by most recently pushed 233 239 # By default, order by most recently pushed
ordering = ['-pushed'] 234 240 ordering = ['-pushed']
235 241
def __unicode__(self): 236 242 def __unicode__(self):
return '<flashcard: %s>' % self.text 237 243 return '<flashcard: %s>' % self.text
238 244
@property 239 245 @property
def material_week_num(self): 240 246 def material_week_num(self):
return (self.material_date - QUARTER_START).days / 7 + 1 241 247 return (self.material_date - QUARTER_START).days / 7 + 1
242 248
def is_hidden_from(self, user): 243 249 def is_hidden_from(self, user):
""" 244 250 """
A card can be hidden globally, but if a user has the card in their deck, 245 251 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 246 252 this visibility overrides a global hide.
:param user: 247 253 :param user:
:return: Whether the card is hidden from the user. 248 254 :return: Whether the card is hidden from the user.
""" 249 255 """
return self.is_hidden or self.flashcardhide_set.filter(user=user).exists() 250 256 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
251 257
def hide_from(self, user, reason=None): 252 258 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 253 259 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 254 260 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 255 261 if not created:
raise ValidationError("The card has already been hidden.") 256 262 raise ValidationError("The card has already been hidden.")
obj.save() 257 263 obj.save()
258 264
def is_in_deck(self, user): 259 265 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 260 266 return self.userflashcard_set.filter(user=user).exists()
261 267
def add_to_deck(self, user): 262 268 def add_to_deck(self, user):
if not user.is_in_section(self.section): 263 269 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 264 270 raise PermissionDenied("You don't have the permission to add this card")
try: 265 271 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 266 272 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 267 273 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 268 274 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 269 275 user_flashcard.save()
return user_flashcard 270 276 return user_flashcard
271 277
def edit(self, user, new_data): 272 278 def edit(self, user, new_data):
""" 273 279 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 274 280 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. 275 281 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 276 282 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 277 283 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 278 284 """
279 285
# content_changed is True iff either material_date or text were changed 280 286 # content_changed is True iff either material_date or text were changed
content_changed = False 281 287 content_changed = False
# create_new is True iff the user editing this card is the author of this card 282 288 # 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 283 289 # and there are no other users with this card in their decks
create_new = user != self.author or \ 284 290 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 285 291 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 286 292 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 287 293 content_changed = True
self.material_date = new_data['material_date'] 288 294 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 289 295 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 290 296 content_changed = True
self.text = new_data['text'] 291 297 self.text = new_data['text']
if create_new and content_changed: 292 298 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 293 299 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 294 300 self.previous_id = self.pk
self.pk = None 295 301 self.pk = None
self.mask = new_data.get('mask', self.mask) 296 302 self.mask = new_data.get('mask', self.mask)
self.save() 297 303 self.save()
self.add_to_deck(user) 298 304 self.add_to_deck(user)
else: 299 305 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 300 306 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 301 307 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 302 308 user_card.save()
return self 303 309 return self
304 310
def report(self, user, reason=None): 305 311 def report(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 306 312 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 307 313 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 308 314 obj.reason = reason
obj.save() 309 315 obj.save()
310 316
@cached_property 311 317 @cached_property
def score(self): 312 318 def score(self):
def seconds_since_epoch(dt): 313 319 def seconds_since_epoch(dt):
from datetime import datetime 314 320 from datetime import datetime
315 321
epoch = make_aware(datetime.utcfromtimestamp(0)) 316 322 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 317 323 delta = dt - epoch
return delta.total_seconds() 318 324 return delta.total_seconds()
319 325
z = 0 320 326 z = 0
rate = 1.0 / 3600 321 327 rate = 1.0 / 3600
for vote in self.userflashcard_set.iterator(): 322 328 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 323 329 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 324 330 u = max(z, rate * t)
v = min(z, rate * t) 325 331 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 326 332 z = u + log1p(exp(v - u))
return z 327 333 return z
328 334
@classmethod 329 335 @classmethod
def cards_visible_to(cls, user): 330 336 def cards_visible_to(cls, user):
""" 331 337 """
:param user: 332 338 :param user:
:return: A queryset with all cards that should be visible to a user. 333 339 :return: A queryset with all cards that should be visible to a user.
""" 334 340 """
return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user) 335 341 return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)
).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 336 342 ).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
337 343
@classmethod 338 344 @classmethod
def cards_hidden_by(cls, user): 339 345 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 340 346 return cls.objects.filter(flashcardhide__user=user)
341 347
342 348
class UserFlashcardQuiz(Model): 343 349 class UserFlashcardQuiz(Model):
""" 344 350 """
An event of a user being quizzed on a flashcard. 345 351 An event of a user being quizzed on a flashcard.
""" 346 352 """
user_flashcard = ForeignKey(UserFlashcard) 347 353 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 348 354 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 349 355 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") 350 356 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") 351 357 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
352 358
def __unicode__(self): 353 359 def __unicode__(self):
return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard)) 354 360 return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard))
355 361
def status(self): 356 362 def status(self):
""" 357 363 """
There are three stages of a quiz object: 358 364 There are three stages of a quiz object:
1. the user has been shown the card 359 365 1. the user has been shown the card
2. the user has answered the card 360 366 2. the user has answered the card
3. the user has self-evaluated their response's correctness 361 367 3. the user has self-evaluated their response's correctness
362 368
:return: string (evaluated, answered, viewed) 363 369 :return: string (evaluated, answered, viewed)
""" 364 370 """
if self.correct is not None: return "evaluated" 365 371 if self.correct is not None: return "evaluated"
if self.response: return "answered" 366 372 if self.response: return "answered"
return "viewed" 367 373 return "viewed"
368 374
369 375
class Section(Model): 370 376 class Section(Model):
""" 371 377 """
A UCSD course taught by an instructor during a quarter. 372 378 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 373 379 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 374 380 We index gratuitously to support autofill and because this is primarily read-only
""" 375 381 """
department = CharField(db_index=True, max_length=50) 376 382 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 377 383 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 378 384 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 379 385 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 380 386 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 381 387 quarter = CharField(db_index=True, max_length=4)
PAGE_SIZE = 40 382 388 PAGE_SIZE = 40
383 389
@classmethod 384 390 @classmethod
def search(cls, terms): 385 391 def search(cls, terms):
""" 386 392 """
Search all fields of all sections for a particular set of terms 387 393 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 388 394 A matching section must match at least one field on each term
:param terms:iterable 389 395 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 390 396 :return: Matching QuerySet ordered by department and course number
""" 391 397 """
final_q = Q() 392 398 final_q = Q()
for term in terms: 393 399 for term in terms:
q = Q(department__icontains=term) 394 400 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 395 401 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 396 402 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 397 403 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 398 404 q |= Q(instructor__icontains=term)
final_q &= q 399 405 final_q &= q
qs = cls.objects.filter(final_q) 400 406 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 401 407 # 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 402 408 # 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)"}) 403 409 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 404 410 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 405 411 return qs
406 412
@property 407 413 @property
def is_whitelisted(self): 408 414 def is_whitelisted(self):
""" 409 415 """
:return: whether a whitelist exists for this section 410 416 :return: whether a whitelist exists for this section
""" 411 417 """
return self.whitelist.exists() 412 418 return self.whitelist.exists()
413 419
def is_user_on_whitelist(self, user): 414 420 def is_user_on_whitelist(self, user):
""" 415 421 """
:return: whether the user is on the waitlist for this section 416 422 :return: whether the user is on the waitlist for this section
""" 417 423 """
return self.whitelist.filter(email=user.email).exists() 418 424 return self.whitelist.filter(email=user.email).exists()
419 425
def is_user_enrolled(self, user): 420 426 def is_user_enrolled(self, user):
return self.user_set.filter(pk=user.pk).exists() 421 427 return self.user_set.filter(pk=user.pk).exists()
422 428
def enroll(self, user): 423 429 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 424 430 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 425 431 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 426 432 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 427 433 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 428 434 self.user_set.add(user)
429 435
def drop(self, user): 430 436 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 431 437 if not user.sections.filter(pk=self.pk).exists():
raise ValidationError("User is not enrolled in the section.") 432 438 raise ValidationError("User is not enrolled in the section.")
self.user_set.remove(user) 433 439 self.user_set.remove(user)
434 440
class Meta: 435 441 class Meta:
ordering = ['department_abbreviation', 'course_num'] 436 442 ordering = ['department_abbreviation', 'course_num']
437 443
@property 438 444 @property
def lecture_times(self): 439 445 def lecture_times(self):
flashcards/serializers.py View file @ 05b5a91
from json import loads 1 1 from json import loads
from collections import Iterable 2 2 from collections import Iterable
3 3
from django.utils.datetime_safe import datetime 4 4 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 5 5 from django.utils.timezone import now
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcardQuiz 6 6 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 7 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 8 8 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \ 9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \
SerializerMethodField, FloatField 10 10 SerializerMethodField, FloatField
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 11 11 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 12 12 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 13 13 from flashy.settings import QUARTER_END, QUARTER_START
14 14
15 15
class EmailSerializer(Serializer): 16 16 class EmailSerializer(Serializer):
email = EmailField(required=True) 17 17 email = EmailField(required=True)
18 18
19 19
class EmailPasswordSerializer(EmailSerializer): 20 20 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 21 21 password = CharField(required=True)
22 22
23 23
class RegistrationSerializer(EmailPasswordSerializer): 24 24 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 25 25 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
26 26
27 27
class PasswordResetRequestSerializer(EmailSerializer): 28 28 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 29 29 def validate_email(self, value):
try: 30 30 try:
User.objects.get(email=value) 31 31 User.objects.get(email=value)
return value 32 32 return value
except User.DoesNotExist: 33 33 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 34 34 raise serializers.ValidationError('No user exists with that email')
35 35
36 36
class PasswordResetSerializer(Serializer): 37 37 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 38 38 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 39 39 uid = IntegerField(required=True)
token = CharField(required=True) 40 40 token = CharField(required=True)
41 41
def validate_uid(self, value): 42 42 def validate_uid(self, value):
try: 43 43 try:
User.objects.get(id=value) 44 44 User.objects.get(id=value)
return value 45 45 return value
except User.DoesNotExist: 46 46 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 47 47 raise serializers.ValidationError('Could not verify reset token')
48 48
49
class EmailVerificationSerializer(Serializer): 49 50 class EmailVerificationSerializer(Serializer):
confirmation_key = CharField() 50 51 confirmation_key = CharField()
51 52
53
class UserUpdateSerializer(Serializer): 52 54 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 53 55 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False)\ 54 56 new_password = CharField(required=False, allow_blank=False) \
# reset_token = CharField(required=False) 55 57 # reset_token = CharField(required=False)
56 58
def validate(self, data): 57 59 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 58 60 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 59 61 raise serializers.ValidationError('old_password is required to set a new_password')
return data 60 62 return data
61 63
64
class LecturePeriodSerializer(ModelSerializer): 62 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 63 66 class Meta:
model = LecturePeriod 64 67 model = LecturePeriod
exclude = 'id', 'section' 65 68 exclude = 'id', 'section'
66 69
67 70
class SectionSerializer(ModelSerializer): 68 71 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 69 72 lecture_times = CharField()
short_name = CharField() 70 73 short_name = CharField()
long_name = CharField() 71 74 long_name = CharField()
can_enroll = SerializerMethodField() 72 75 can_enroll = SerializerMethodField()
is_enrolled = SerializerMethodField() 73 76 is_enrolled = SerializerMethodField()
74 77
class Meta: 75 78 class Meta:
model = Section 76 79 model = Section
77 80
def get_can_enroll(self, obj): 78 81 def get_can_enroll(self, obj):
if 'user' not in self.context: return False 79 82 if 'user' not in self.context: return False
if not obj.is_whitelisted: return True 80 83 if not obj.is_whitelisted: return True
return obj.is_user_on_whitelist(self.context['user']) 81 84 return obj.is_user_on_whitelist(self.context['user'])
82 85
def get_is_enrolled(self, obj): 83 86 def get_is_enrolled(self, obj):
if 'user' not in self.context: return False 84 87 if 'user' not in self.context: return False
return obj.is_user_enrolled(self.context['user']) 85 88 return obj.is_user_enrolled(self.context['user'])
86 89
87 90
class DeepSectionSerializer(SectionSerializer): 88 91 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 89 92 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
90 93
91 94
class FeedRequestSerializer(Serializer): 92 95 class FeedRequestSerializer(Serializer):
page = IntegerField(min_value=1, default=1, required=False) 93 96 page = IntegerField(min_value=1, default=1, required=False)
94 97
def validate(self, attrs): 95 98 def validate(self, attrs):
if not isinstance(attrs['page'], int): 96 99 if not isinstance(attrs['page'], int):
raise serializers.ValidationError("Invalid page number") 97 100 raise serializers.ValidationError("Invalid page number")
return attrs 98 101 return attrs
99 102
100 103
class UserSerializer(ModelSerializer): 101 104 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 102 105 email = EmailField(required=False)
sections = SectionSerializer(many=True) 103 106 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 104
105 107
class Meta: 106 108 class Meta:
model = User 107 109 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 108 110 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined", 'locked')
109 111
110 112
class MaskFieldSerializer(serializers.Field): 111 113 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 112 114 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 113 115 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 114 116 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 115 117 'overlap': 'Ensure this field does not have overlapping intervals.'
} 116 118 }
117 119
def to_representation(self, value): 118 120 def to_representation(self, value):
return map(list, self._make_mask(value)) 119 121 return map(list, self._make_mask(value))
120 122
def to_internal_value(self, value): 121 123 def to_internal_value(self, value):
if not isinstance(value, list): 122 124 if not isinstance(value, list):
value = loads(value) 123 125 value = loads(value)
return self._make_mask(value) 124 126 return self._make_mask(value)
125 127
def _make_mask(self, data): 126 128 def _make_mask(self, data):
try: 127 129 try:
mask = FlashcardMask(data) 128 130 mask = FlashcardMask(data)
except ValueError: 129 131 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 130 132 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 131 133 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 132 134 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 133 135 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 134 136 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 135 137 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 136 138 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 137 139 return mask
138 140
139 141
class FlashcardSerializer(ModelSerializer): 140 142 class FlashcardSerializer(ModelSerializer):
is_hidden = SerializerMethodField() 141 143 is_hidden = SerializerMethodField()
is_in_deck = SerializerMethodField() 142 144 is_in_deck = SerializerMethodField()
# hide_reason = CharField(read_only=True) 143 145 # hide_reason = CharField(read_only=True)
material_week_num = IntegerField(read_only=True) 144 146 material_week_num = IntegerField(read_only=True)
material_date = DateTimeField(default=now) 145 147 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 146 148 mask = MaskFieldSerializer(allow_null=True)
score = FloatField(read_only=True) 147 149 score = FloatField(read_only=True)
148 150
def validate_material_date(self, value): 149 151 def validate_material_date(self, value):
# TODO: make this dynamic 150 152 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 151 153 if QUARTER_START <= value <= QUARTER_END:
return value 152 154 return value
else: 153 155 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 154 156 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
155 157
def validate_pushed(self, value): 156 158 def validate_pushed(self, value):
if value > datetime.now(): 157 159 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 158 160 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 159 161 return value
160 162
def validate_mask(self, value): 161 163 def validate_mask(self, value):
if value is None: 162 164 if value is None:
return None 163 165 return None
if len(self.initial_data['text']) < value.max_offset(): 164 166 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 165 167 raise serializers.ValidationError("Mask out of bounds")
return value 166 168 return value
167 169
def get_is_hidden(self, obj): 168 170 def get_is_hidden(self, obj):
if 'user' not in self.context: return False 169 171 if 'user' not in self.context: return False
return obj.is_hidden_from(self.context['user']) 170 172 return obj.is_hidden_from(self.context['user'])
171 173
def get_is_in_deck(self, obj): 172 174 def get_is_in_deck(self, obj):
if 'user' not in self.context: return False 173 175 if 'user' not in self.context: return False
return obj.is_in_deck(self.context['user']) 174 176 return obj.is_in_deck(self.context['user'])
175 177
class Meta: 176 178 class Meta:
model = Flashcard 177 179 model = Flashcard
exclude = 'author', 'previous' 178 180 exclude = 'author', 'previous'
179 181
180 182
class FlashcardUpdateSerializer(serializers.Serializer): 181 183 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 182 184 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 183 185 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 184 186 mask = MaskFieldSerializer(required=False)
185 187
def validate_material_date(self, date): 186 188 def validate_material_date(self, date):
if date > QUARTER_END: 187 189 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 188 190 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 189 191 return date
190 192
def validate(self, attrs): 191 193 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 192 194 # 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']): 193 195 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 194 196 raise serializers.ValidationError("No new value passed in")
return attrs 195 197 return attrs
196 198
197 199
class QuizRequestSerializer(serializers.Serializer): 198 200 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False) 199 201 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 200 202 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 201 203 material_date_end = DateTimeField(default=QUARTER_END)
202 204
def update(self, instance, validated_data): 203 205 def update(self, instance, validated_data):
pass 204 206 pass
205 207
def create(self, validated_data): 206 208 def create(self, validated_data):
return validated_data 207 209 return validated_data
208 210
def validate_material_date_begin(self, value): 209 211 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 210 212 if QUARTER_START <= value <= QUARTER_END:
return value 211 213 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 212 214 raise serializers.ValidationError("Invalid begin date for the flashcard range")
213 215
def validate_material_date_end(self, value): 214 216 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 215 217 if QUARTER_START <= value <= QUARTER_END:
return value 216 218 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 217 219 raise serializers.ValidationError("Invalid end date for the flashcard range")
218 220
def validate_sections(self, value): 219 221 def validate_sections(self, value):
if value is not None and not isinstance(value, Iterable): 220 222 if value is not None and not isinstance(value, Iterable):
raise serializers.ValidationError("Invalid section format. Expecting a list or no value.") 221 223 raise serializers.ValidationError("Invalid section format. Expecting a list or no value.")
if value is None or len(value) == 0: 222 224 if value is None or len(value) == 0:
return Section.objects.all() 223 225 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 224 226 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 225 227 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 226 228 raise serializers.ValidationError("Those aren't valid sections")
return value 227 229 return value
228 230
def validate(self, attrs): 229 231 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 230 232 if attrs['material_date_begin'] > attrs['material_date_end']:
raise serializers.ValidationError("Invalid range") 231 233 raise serializers.ValidationError("Invalid range")
if 'sections' not in attrs: 232 234 if 'sections' not in attrs: