Commit ed0437a1383120a0928e30934fe613d8fffac189

Authored by Andrew Buss
1 parent 9d3d5e9047
Exists in master

instance.pk instead of instance.user_flashcard.pk

Showing 2 changed files with 1 additions and 2 deletions Inline Diff

flashcards/serializers.py View file @ ed0437a
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 49
class EmailVerificationSerializer(Serializer): 50 50 class EmailVerificationSerializer(Serializer):
confirmation_key = CharField() 51 51 confirmation_key = CharField()
52 52
53 53
class UserUpdateSerializer(Serializer): 54 54 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 55 55 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 56 56 new_password = CharField(required=False, allow_blank=False)
57 57
def validate(self, data): 58 58 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 59 59 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 60 60 raise serializers.ValidationError('old_password is required to set a new_password')
return data 61 61 return data
62 62
63 63
class LecturePeriodSerializer(ModelSerializer): 64 64 class LecturePeriodSerializer(ModelSerializer):
class Meta: 65 65 class Meta:
model = LecturePeriod 66 66 model = LecturePeriod
exclude = 'id', 'section' 67 67 exclude = 'id', 'section'
68 68
69 69
class SectionSerializer(ModelSerializer): 70 70 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 71 71 lecture_times = CharField()
short_name = CharField() 72 72 short_name = CharField()
long_name = CharField() 73 73 long_name = CharField()
can_enroll = SerializerMethodField() 74 74 can_enroll = SerializerMethodField()
is_enrolled = SerializerMethodField() 75 75 is_enrolled = SerializerMethodField()
76 76
class Meta: 77 77 class Meta:
model = Section 78 78 model = Section
79 79
def get_can_enroll(self, obj): 80 80 def get_can_enroll(self, obj):
if 'user' not in self.context: return False 81 81 if 'user' not in self.context: return False
if not obj.is_whitelisted: return True 82 82 if not obj.is_whitelisted: return True
return obj.is_user_on_whitelist(self.context['user']) 83 83 return obj.is_user_on_whitelist(self.context['user'])
84 84
def get_is_enrolled(self, obj): 85 85 def get_is_enrolled(self, obj):
if 'user' not in self.context: return False 86 86 if 'user' not in self.context: return False
return obj.is_user_enrolled(self.context['user']) 87 87 return obj.is_user_enrolled(self.context['user'])
88 88
89 89
class DeepSectionSerializer(SectionSerializer): 90 90 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 91 91 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
92 92
93 93
class FeedRequestSerializer(Serializer): 94 94 class FeedRequestSerializer(Serializer):
page = IntegerField(min_value=1, default=1, required=False) 95 95 page = IntegerField(min_value=1, default=1, required=False)
96 96
def validate(self, attrs): 97 97 def validate(self, attrs):
if not isinstance(attrs['page'], int): 98 98 if not isinstance(attrs['page'], int):
raise serializers.ValidationError("Invalid page number") 99 99 raise serializers.ValidationError("Invalid page number")
return attrs 100 100 return attrs
101 101
102 102
class UserSerializer(ModelSerializer): 103 103 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 104 104 email = EmailField(required=False)
sections = SectionSerializer(many=True) 105 105 sections = SectionSerializer(many=True)
106 106
class Meta: 107 107 class Meta:
model = User 108 108 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined", 'locked') 109 109 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined", 'locked')
110 110
111 111
class MaskFieldSerializer(serializers.Field): 112 112 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 113 113 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 114 114 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 115 115 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 116 116 'overlap': 'Ensure this field does not have overlapping intervals.'
} 117 117 }
118 118
def to_representation(self, value): 119 119 def to_representation(self, value):
return map(list, self._make_mask(value)) 120 120 return map(list, self._make_mask(value))
121 121
def to_internal_value(self, value): 122 122 def to_internal_value(self, value):
if not isinstance(value, list): 123 123 if not isinstance(value, list):
value = loads(value) 124 124 value = loads(value)
return self._make_mask(value) 125 125 return self._make_mask(value)
126 126
def _make_mask(self, data): 127 127 def _make_mask(self, data):
try: 128 128 try:
mask = FlashcardMask(data) 129 129 mask = FlashcardMask(data)
except ValueError: 130 130 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 131 131 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 132 132 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 133 133 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 134 134 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 135 135 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 136 136 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 137 137 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 138 138 return mask
139 139
140 140
class FlashcardSerializer(ModelSerializer): 141 141 class FlashcardSerializer(ModelSerializer):
is_hidden = SerializerMethodField() 142 142 is_hidden = SerializerMethodField()
is_in_deck = SerializerMethodField() 143 143 is_in_deck = SerializerMethodField()
material_week_num = IntegerField(read_only=True) 144 144 material_week_num = IntegerField(read_only=True)
material_date = DateTimeField(default=now) 145 145 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 146 146 mask = MaskFieldSerializer(allow_null=True)
score = FloatField(read_only=True) 147 147 score = FloatField(read_only=True)
148 148
def validate_material_date(self, value): 149 149 def validate_material_date(self, value):
# TODO: make this dynamic 150 150 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 151 151 if QUARTER_START <= value <= QUARTER_END:
return value 152 152 return value
else: 153 153 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 154 154 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
155 155
def validate_pushed(self, value): 156 156 def validate_pushed(self, value):
if value > datetime.now(): 157 157 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 158 158 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 159 159 return value
160 160
def validate_mask(self, value): 161 161 def validate_mask(self, value):
if value is None: 162 162 if value is None:
return None 163 163 return None
if len(self.initial_data['text']) < value.max_offset(): 164 164 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 165 165 raise serializers.ValidationError("Mask out of bounds")
return value 166 166 return value
167 167
def get_is_hidden(self, obj): 168 168 def get_is_hidden(self, obj):
if 'user' not in self.context: return False 169 169 if 'user' not in self.context: return False
return obj.is_hidden_from(self.context['user']) 170 170 return obj.is_hidden_from(self.context['user'])
171 171
def get_is_in_deck(self, obj): 172 172 def get_is_in_deck(self, obj):
if 'user' not in self.context: return False 173 173 if 'user' not in self.context: return False
return obj.is_in_deck(self.context['user']) 174 174 return obj.is_in_deck(self.context['user'])
175 175
class Meta: 176 176 class Meta:
model = Flashcard 177 177 model = Flashcard
exclude = 'author', 'previous', 'hide_reason' 178 178 exclude = 'author', 'previous', 'hide_reason'
179 179
180 180
class FlashcardUpdateSerializer(serializers.Serializer): 181 181 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 182 182 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 183 183 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 184 184 mask = MaskFieldSerializer(required=False)
185 185
def validate_material_date(self, date): 186 186 def validate_material_date(self, date):
if date > QUARTER_END: 187 187 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 188 188 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 189 189 return date
190 190
def validate(self, attrs): 191 191 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 192 192 # 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 193 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 194 194 raise serializers.ValidationError("No new value passed in")
return attrs 195 195 return attrs
196 196
197 197
class QuizRequestSerializer(serializers.Serializer): 198 198 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False, default=[]) 199 199 sections = ListField(child=IntegerField(min_value=1), required=False, default=[])
material_date_begin = DateTimeField(default=QUARTER_START) 200 200 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 201 201 material_date_end = DateTimeField(default=QUARTER_END)
202 202
def update(self, instance, validated_data): 203 203 def update(self, instance, validated_data):
pass 204 204 pass
205 205
def create(self, validated_data): 206 206 def create(self, validated_data):
return validated_data 207 207 return validated_data
208 208
def validate_material_date_begin(self, value): 209 209 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 210 210 if QUARTER_START <= value <= QUARTER_END:
return value 211 211 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 212 212 raise serializers.ValidationError("Invalid begin date for the flashcard range")
213 213
def validate_material_date_end(self, value): 214 214 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 215 215 if QUARTER_START <= value <= QUARTER_END:
return value 216 216 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 217 217 raise serializers.ValidationError("Invalid end date for the flashcard range")
218 218
def validate_sections(self, value): 219 219 def validate_sections(self, value):
if value is not None and not isinstance(value, Iterable): 220 220 if value is not None and not isinstance(value, Iterable):
raise serializers.ValidationError("Invalid section format. Expecting a list or no value.") 221 221 raise serializers.ValidationError("Invalid section format. Expecting a list or no value.")
if value is None or len(value) == 0: 222 222 if value is None or len(value) == 0:
return Section.objects.all() 223 223 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 224 224 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 225 225 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 226 226 raise serializers.ValidationError("Those aren't valid sections")
return value 227 227 return value
228 228
def validate(self, attrs): 229 229 def validate(self, attrs):
flashcards/views.py View file @ ed0437a
import django 1 1 import django
from django.contrib import auth 2 2 from django.contrib import auth
from django.shortcuts import get_object_or_404 3 3 from django.shortcuts import get_object_or_404
from django.utils.log import getLogger 4 4 from django.utils.log import getLogger
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \ 5 5 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \
IsAuthenticatedAndConfirmed 6 6 IsAuthenticatedAndConfirmed
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \ 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \
FlashcardAlreadyPulledException, FlashcardNotInDeckException, FlashcardAlreadyHiddenException 8 8 FlashcardAlreadyPulledException, FlashcardNotInDeckException, FlashcardAlreadyHiddenException
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 9 9 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 10 10 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, SubscribeViewSerializer, \ 11 11 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, SubscribeViewSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer 12 12 QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route, throttle_classes 13 13 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route, throttle_classes
from rest_framework.generics import ListAPIView, GenericAPIView 14 14 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 15 15 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 16 16 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 17 17 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 18 18 from django.core.mail import send_mail
from django.contrib.auth import authenticate 19 19 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 20 20 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 21 21 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 22 22 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied, \ 23 23 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied, \
Throttled 24 24 Throttled
from simple_email_confirmation import EmailAddress 25 25 from simple_email_confirmation import EmailAddress
from rest_framework.throttling import UserRateThrottle 26 26 from rest_framework.throttling import UserRateThrottle
27 27
def log_event(request, event=''): 28 28 def log_event(request, event=''):
logstr = u'%s %s %s %s' % (request.META['REMOTE_ADDR'], request.user, request.path, event) 29 29 logstr = u'%s %s %s %s' % (request.META['REMOTE_ADDR'], request.user, request.path, event)
getLogger('flashy.events').info(logstr) 30 30 getLogger('flashy.events').info(logstr)
31 31
class LimitFlashcardPushThrottle(UserRateThrottle): 32 32 class LimitFlashcardPushThrottle(UserRateThrottle):
rate = '10/min' 33 33 rate = '10/min'
34 34
class SectionViewSet(ReadOnlyModelViewSet): 35 35 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 36 36 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 37 37 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 38 38 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticatedAndConfirmed] 39 39 permission_classes = [IsAuthenticatedAndConfirmed]
40 40
@detail_route(methods=['GET']) 41 41 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 42 42 def flashcards(self, request, pk):
""" 43 43 """
Gets flashcards for a section, excluding hidden cards. 44 44 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 45 45 Returned in strictly chronological order (material date).
""" 46 46 """
flashcards = Flashcard.cards_visible_to(request.user) 47 47 flashcards = Flashcard.cards_visible_to(request.user)
if 'hidden' in request.GET: 48 48 if 'hidden' in request.GET:
if request.GET['hidden'] == 'only': 49 49 if request.GET['hidden'] == 'only':
flashcards = Flashcard.cards_hidden_by(request.user) 50 50 flashcards = Flashcard.cards_hidden_by(request.user)
else: 51 51 else:
flashcards |= Flashcard.cards_hidden_by(request.user) 52 52 flashcards |= Flashcard.cards_hidden_by(request.user)
flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all() 53 53 flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all()
log_event(request, str(self.get_object())) 54 54 log_event(request, str(self.get_object()))
return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data) 55 55 return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data)
56 56
@detail_route(methods=['POST']) 57 57 @detail_route(methods=['POST'])
def enroll(self, request, pk): 58 58 def enroll(self, request, pk):
59 59
""" 60 60 """
Add the current user to a specified section 61 61 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. 62 62 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 63 63 ---
view_mocker: flashcards.api.mock_no_params 64 64 view_mocker: flashcards.api.mock_no_params
""" 65 65 """
try: 66 66 try:
self.get_object().enroll(request.user) 67 67 self.get_object().enroll(request.user)
log_event(request, str(self.get_object())) 68 68 log_event(request, str(self.get_object()))
except django.core.exceptions.PermissionDenied as e: 69 69 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 70 70 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 71 71 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 72 72 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 73 73 return Response(status=HTTP_204_NO_CONTENT)
74 74
@detail_route(methods=['POST']) 75 75 @detail_route(methods=['POST'])
def drop(self, request, pk): 76 76 def drop(self, request, pk):
""" 77 77 """
Remove the current user from a specified section 78 78 Remove the current user from a specified section
If the user is not in the class, the request will fail. 79 79 If the user is not in the class, the request will fail.
--- 80 80 ---
view_mocker: flashcards.api.mock_no_params 81 81 view_mocker: flashcards.api.mock_no_params
""" 82 82 """
try: 83 83 try:
self.get_object().drop(request.user) 84 84 self.get_object().drop(request.user)
log_event(request, str(self.get_object())) 85 85 log_event(request, str(self.get_object()))
except django.core.exceptions.PermissionDenied as e: 86 86 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 87 87 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 88 88 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 89 89 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 90 90 return Response(status=HTTP_204_NO_CONTENT)
91 91
@list_route(methods=['GET']) 92 92 @list_route(methods=['GET'])
def search(self, request): 93 93 def search(self, request):
""" 94 94 """
Returns a list of sections which match a user's query 95 95 Returns a list of sections which match a user's query
--- 96 96 ---
parameters: 97 97 parameters:
- name: q 98 98 - name: q
description: space-separated list of terms 99 99 description: space-separated list of terms
required: true 100 100 required: true
type: form 101 101 type: form
response_serializer: SectionSerializer 102 102 response_serializer: SectionSerializer
""" 103 103 """
query = request.GET.get('q', None) 104 104 query = request.GET.get('q', None)
if not query: return Response('[]') 105 105 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 106 106 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True, context={'user': request.user}).data 107 107 data = SectionSerializer(qs, many=True, context={'user': request.user}).data
log_event(request, query) 108 108 log_event(request, query)
return Response(data) 109 109 return Response(data)
110 110
@detail_route(methods=['GET']) 111 111 @detail_route(methods=['GET'])
def deck(self, request, pk): 112 112 def deck(self, request, pk):
""" 113 113 """
Gets the contents of a user's deck for a given section. 114 114 Gets the contents of a user's deck for a given section.
""" 115 115 """
qs = request.user.get_deck(self.get_object()) 116 116 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 117 117 serializer = FlashcardSerializer(qs, many=True)
log_event(request, str(self.get_object())) 118 118 log_event(request, str(self.get_object()))
return Response(serializer.data) 119 119 return Response(serializer.data)
120 120
@detail_route(methods=['GET']) 121 121 @detail_route(methods=['GET'])
def feed(self, request, pk): 122 122 def feed(self, request, pk):
""" 123 123 """
Gets the contents of a user's feed for a section. 124 124 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 125 125 Exclude cards that are already in the user's deck
request_serializer: FeedRequestSerializer 126 126 request_serializer: FeedRequestSerializer
response_serializer: FlashcardSerializer 127 127 response_serializer: FlashcardSerializer
""" 128 128 """
feed_serializer = FeedRequestSerializer(data=request.data) 129 129 feed_serializer = FeedRequestSerializer(data=request.data)
feed_serializer.is_valid(raise_exception=True) 130 130 feed_serializer.is_valid(raise_exception=True)
page = feed_serializer.validated_data['page'] 131 131 page = feed_serializer.validated_data['page']
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user, page=page), 132 132 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user, page=page),
many=True, context={'user': request.user}) 133 133 many=True, context={'user': request.user})
log_event(request, str(self.get_object())) 134 134 log_event(request, str(self.get_object()))
return Response(serializer.data) 135 135 return Response(serializer.data)
136 136
137 137
class UserSectionListView(ListAPIView): 138 138 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 139 139 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticatedAndConfirmed] 140 140 permission_classes = [IsAuthenticatedAndConfirmed]
141 141
def get_queryset(self): 142 142 def get_queryset(self):
return self.request.user.sections.all() 143 143 return self.request.user.sections.all()
144 144
def paginate_queryset(self, queryset): return None 145 145 def paginate_queryset(self, queryset): return None
146 146
147 147
class UserDetail(GenericAPIView): 148 148 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 149 149 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 150 150 permission_classes = [IsAuthenticated]
151 151
def patch(self, request, format=None): 152 152 def patch(self, request, format=None):
""" 153 153 """
Updates the user's password 154 154 Updates the user's password
--- 155 155 ---
request_serializer: UserUpdateSerializer 156 156 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 157 157 response_serializer: UserSerializer
""" 158 158 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 159 159 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 160 160 data.is_valid(raise_exception=True)
data = data.validated_data 161 161 data = data.validated_data
162 162
if 'new_password' in data: 163 163 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 164 164 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 165 165 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 166 166 request.user.set_password(data['new_password'])
request.user.save() 167 167 request.user.save()
log_event(request, 'change password') 168 168 log_event(request, 'change password')
169 169
return Response(UserSerializer(request.user).data) 170 170 return Response(UserSerializer(request.user).data)
171 171
def get(self, request, format=None): 172 172 def get(self, request, format=None):
""" 173 173 """
Return data about the user 174 174 Return data about the user
--- 175 175 ---
response_serializer: UserSerializer 176 176 response_serializer: UserSerializer
""" 177 177 """
serializer = UserSerializer(request.user, context={'request': request}) 178 178 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 179 179 return Response(serializer.data)
180 180
def delete(self, request): 181 181 def delete(self, request):
""" 182 182 """
Irrevocably delete the user and their data 183 183 Irrevocably delete the user and their data
184 184
Yes, really 185 185 Yes, really
""" 186 186 """
request.user.delete() 187 187 request.user.delete()
log_event(request) 188 188 log_event(request)
return Response(status=HTTP_204_NO_CONTENT) 189 189 return Response(status=HTTP_204_NO_CONTENT)
190 190
191 191
@api_view(['POST']) 192 192 @api_view(['POST'])
@permission_classes([IsAuthenticated]) 193 193 @permission_classes([IsAuthenticated])
def resend_confirmation_email(request): 194 194 def resend_confirmation_email(request):
"Resends a confirmation email to a user" 195 195 "Resends a confirmation email to a user"
request.user.send_confirmation_email() 196 196 request.user.send_confirmation_email()
return Response(status=HTTP_204_NO_CONTENT) 197 197 return Response(status=HTTP_204_NO_CONTENT)
198 198
199 199
@api_view(['POST']) 200 200 @api_view(['POST'])
def verify_email(request): 201 201 def verify_email(request):
""" 202 202 """
Accepts a user's email confirmation_key to verify their email address 203 203 Accepts a user's email confirmation_key to verify their email address
--- 204 204 ---
request_serializer: EmailVerificationSerializer 205 205 request_serializer: EmailVerificationSerializer
""" 206 206 """
207 207
data = EmailVerificationSerializer(data=request.data) 208 208 data = EmailVerificationSerializer(data=request.data)
data.is_valid(raise_exception=True) 209 209 data.is_valid(raise_exception=True)
try: 210 210 try:
email = User.confirm_email(data.validated_data['confirmation_key']) 211 211 email = User.confirm_email(data.validated_data['confirmation_key'])
except EmailAddress.DoesNotExist: 212 212 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 213 213 raise ValidationError('confirmation_key is invalid')
log_event(request, 'confirm email' + str(email)) 214 214 log_event(request, 'confirm email' + str(email))
return Response(status=HTTP_204_NO_CONTENT) 215 215 return Response(status=HTTP_204_NO_CONTENT)
216 216
217 217
@api_view(['POST']) 218 218 @api_view(['POST'])
def register(request, format=None): 219 219 def register(request, format=None):
""" 220 220 """
Register a new user 221 221 Register a new user
--- 222 222 ---
request_serializer: EmailPasswordSerializer 223 223 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 224 224 response_serializer: UserSerializer
""" 225 225 """
data = RegistrationSerializer(data=request.data) 226 226 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 227 227 data.is_valid(raise_exception=True)
228 228
User.objects.create_user(**data.validated_data) 229 229 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 230 230 user = authenticate(**data.validated_data)
auth.login(request, user) 231 231 auth.login(request, user)
log_event(request) 232 232 log_event(request)
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 233 233 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
234 234
235 235
@api_view(['POST']) 236 236 @api_view(['POST'])
def subscribe(request, format=None): 237 237 def subscribe(request, format=None):
""" 238 238 """
Associate the user with the passed in registration token 239 239 Associate the user with the passed in registration token
--- 240 240 ---
request_serializer: SubscribeViewSerializer 241 241 request_serializer: SubscribeViewSerializer
""" 242 242 """
serializer = SubscribeViewSerializer(data=request.data) 243 243 serializer = SubscribeViewSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 244 244 serializer.is_valid(raise_exception=True)
request.user.set_registration_id(serializer.validated_data['registration_id']) 245 245 request.user.set_registration_id(serializer.validated_data['registration_id'])
return Response(status=HTTP_204_NO_CONTENT) 246 246 return Response(status=HTTP_204_NO_CONTENT)
247 247
248 248
@api_view(['POST']) 249 249 @api_view(['POST'])
def login(request): 250 250 def login(request):
""" 251 251 """
Authenticates user and returns user data if valid. 252 252 Authenticates user and returns user data if valid.
--- 253 253 ---
request_serializer: EmailPasswordSerializer 254 254 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 255 255 response_serializer: UserSerializer
""" 256 256 """
257 257
data = EmailPasswordSerializer(data=request.data) 258 258 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 259 259 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 260 260 user = authenticate(**data.validated_data)
261 261
if user is None: 262 262 if user is None:
raise AuthenticationFailed('Invalid email or password') 263 263 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 264 264 if not user.is_active:
raise NotAuthenticated('Account is disabled') 265 265 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 266 266 auth.login(request, user)
log_event(request) 267 267 log_event(request)
return Response(UserSerializer(request.user).data) 268 268 return Response(UserSerializer(request.user).data)
269 269
270 270
@api_view(['POST']) 271 271 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 272 272 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 273 273 def logout(request, format=None):
""" 274 274 """
Logs the authenticated user out. 275 275 Logs the authenticated user out.
""" 276 276 """
auth.logout(request) 277 277 auth.logout(request)
log_event(request) 278 278 log_event(request)
return Response(status=HTTP_204_NO_CONTENT) 279 279 return Response(status=HTTP_204_NO_CONTENT)
280 280
281 281
@api_view(['POST']) 282 282 @api_view(['POST'])
def request_password_reset(request, format=None): 283 283 def request_password_reset(request, format=None):
""" 284 284 """
Send a password reset token/link to the provided email. 285 285 Send a password reset token/link to the provided email.
--- 286 286 ---
request_serializer: PasswordResetRequestSerializer 287 287 request_serializer: PasswordResetRequestSerializer
""" 288 288 """
data = PasswordResetRequestSerializer(data=request.data) 289 289 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 290 290 data.is_valid(raise_exception=True)
log_event(request, 'email: ' + str(data['email'])) 291 291 log_event(request, 'email: ' + str(data['email']))
get_object_or_404(User, email=data['email'].value).request_password_reset() 292 292 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 293 293 return Response(status=HTTP_204_NO_CONTENT)
294 294
295 295
@api_view(['POST']) 296 296 @api_view(['POST'])
def reset_password(request, format=None): 297 297 def reset_password(request, format=None):
""" 298 298 """
Updates user's password to new password if token is valid. 299 299 Updates user's password to new password if token is valid.
--- 300 300 ---
request_serializer: PasswordResetSerializer 301 301 request_serializer: PasswordResetSerializer
""" 302 302 """
data = PasswordResetSerializer(data=request.data) 303 303 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 304 304 data.is_valid(raise_exception=True)
305 305
user = User.objects.get(id=data['uid'].value) 306 306 user = User.objects.get(id=data['uid'].value)
# Check token validity. 307 307 # Check token validity.
308 308
if default_token_generator.check_token(user, data['token'].value): 309 309 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 310 310 user.set_password(data['new_password'].value)
user.save() 311 311 user.save()
user = authenticate(email=user.email, password=data['new_password'].value) 312 312 user = authenticate(email=user.email, password=data['new_password'].value)
auth.login(request, user) 313 313 auth.login(request, user)
314 314
log_event(request) 315 315 log_event(request)
else: 316 316 else:
raise ValidationError('Could not verify reset token') 317 317 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 318 318 return Response(status=HTTP_204_NO_CONTENT)
319 319
320 320
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 321 321 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 322 322 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 323 323 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection] 324 324 permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection]
325 325
# Override create in CreateModelMixin 326 326 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 327 327 def create(self, request, *args, **kwargs):
if not LimitFlashcardPushThrottle().allow_request(request, 'flashcard_create'): 328 328 if not LimitFlashcardPushThrottle().allow_request(request, 'flashcard_create'):
raise Throttled(wait=None, detail=None) 329 329 raise Throttled(wait=None, detail=None)
serializer = FlashcardSerializer(data=request.data) 330 330 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 331 331 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 332 332 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 333 333 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 334 334 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 335 335 data['author'] = request.user
flashcard = Flashcard.push(**data) 336 336 flashcard = Flashcard.push(**data)
response_data = FlashcardSerializer(flashcard).data 337 337 response_data = FlashcardSerializer(flashcard).data
log_event(request, response_data) 338 338 log_event(request, response_data)
headers = self.get_success_headers(data) 339 339 headers = self.get_success_headers(data)
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 340 340 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
341 341
@detail_route(methods=['POST']) 342 342 @detail_route(methods=['POST'])
def unhide(self, request, pk): 343 343 def unhide(self, request, pk):
""" 344 344 """
Unhide the given card 345 345 Unhide the given card
--- 346 346 ---
view_mocker: flashcards.api.mock_no_params 347 347 view_mocker: flashcards.api.mock_no_params
""" 348 348 """
try: 349 349 try:
self.get_object().unhide_by_user(request.user) 350 350 self.get_object().unhide_by_user(request.user)
except FlashcardHide.DoesNotExist: 351 351 except FlashcardHide.DoesNotExist:
raise ValidationError("Cannot unhide a card which is not hidden") 352 352 raise ValidationError("Cannot unhide a card which is not hidden")
log_event(request, unicode(self.get_object())) 353 353 log_event(request, unicode(self.get_object()))
return Response(status=HTTP_204_NO_CONTENT) 354 354 return Response(status=HTTP_204_NO_CONTENT)
355 355
@detail_route(methods=['POST']) 356 356 @detail_route(methods=['POST'])
def report(self, request, pk): 357 357 def report(self, request, pk):
""" 358 358 """
Hide the given card 359 359 Hide the given card
--- 360 360 ---
view_mocker: flashcards.api.mock_no_params 361 361 view_mocker: flashcards.api.mock_no_params
""" 362 362 """
try: 363 363 try:
self.get_object().hide_by_user(request.user) 364 364 self.get_object().hide_by_user(request.user)
except FlashcardAlreadyHiddenException: 365 365 except FlashcardAlreadyHiddenException:
raise ValidationError('Cannot hide a card which is already hidden') 366 366 raise ValidationError('Cannot hide a card which is already hidden')
log_event(request, unicode(self.get_object())) 367 367 log_event(request, unicode(self.get_object()))
return Response(status=HTTP_204_NO_CONTENT) 368 368 return Response(status=HTTP_204_NO_CONTENT)
369 369
hide = report 370 370 hide = report
371 371
@detail_route(methods=['POST']) 372 372 @detail_route(methods=['POST'])
def pull(self, request, pk): 373 373 def pull(self, request, pk):
""" 374 374 """
Pull a card from the live feed into the user's deck. 375 375 Pull a card from the live feed into the user's deck.
--- 376 376 ---
view_mocker: flashcards.api.mock_no_params 377 377 view_mocker: flashcards.api.mock_no_params
""" 378 378 """
try: 379 379 try:
request.user.pull(self.get_object()) 380 380 request.user.pull(self.get_object())
log_event(request, self.get_object()) 381 381 log_event(request, self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 382 382 return Response(status=HTTP_204_NO_CONTENT)
except FlashcardAlreadyPulledException: 383 383 except FlashcardAlreadyPulledException:
raise ValidationError('Cannot pull a card already in deck') 384 384 raise ValidationError('Cannot pull a card already in deck')
385 385
@detail_route(methods=['POST']) 386 386 @detail_route(methods=['POST'])
def unpull(self, request, pk): 387 387 def unpull(self, request, pk):
""" 388 388 """
Unpull a card from the user's deck 389 389 Unpull a card from the user's deck
--- 390 390 ---
view_mocker: flashcards.api.mock_no_params 391 391 view_mocker: flashcards.api.mock_no_params
""" 392 392 """
user = request.user 393 393 user = request.user
flashcard = self.get_object() 394 394 flashcard = self.get_object()
try: 395 395 try:
user.unpull(flashcard) 396 396 user.unpull(flashcard)
log_event(request, self.get_object()) 397 397 log_event(request, self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 398 398 return Response(status=HTTP_204_NO_CONTENT)
except FlashcardNotInDeckException: 399 399 except FlashcardNotInDeckException:
raise ValidationError('Cannot unpull a card not in deck') 400 400 raise ValidationError('Cannot unpull a card not in deck')
401 401
def partial_update(self, request, *args, **kwargs): 402 402 def partial_update(self, request, *args, **kwargs):
""" 403 403 """
Edit settings related to a card for the user. 404 404 Edit settings related to a card for the user.
--- 405 405 ---