Commit 7fd48b304d59b4fafbfe84acae4b2c7ea9671e2d

Authored by Rachel Lee
Exists in master

Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend

Showing 5 changed files Inline Diff

flashcards/fields.py View file @ 7fd48b3
from django.db import models 1 1 from django.db import models
2 from validators import FlashcardMask, OverlapIntervalException
2 3
3 4
class MaskField(models.Field): 4 5 class MaskField(models.Field):
def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): 5 6 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
self.blank_sep = blank_sep 6 7 self.blank_sep = blank_sep
self.range_sep = range_sep 7 8 self.range_sep = range_sep
super(MaskField, self).__init__(*args, **kwargs) 8 9 super(MaskField, self).__init__(*args, **kwargs)
9 10
@staticmethod 10 11 @staticmethod
def _using_array(connection): 11 12 def _using_array(connection):
return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2' 12 13 return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
13 14
def deconstruct(self): 14 15 def deconstruct(self):
name, path, args, kwargs = super(MaskField, self).deconstruct() 15 16 name, path, args, kwargs = super(MaskField, self).deconstruct()
kwargs['blank_sep'] = self.blank_sep 16 17 kwargs['blank_sep'] = self.blank_sep
kwargs['range_sep'] = self.range_sep 17 18 kwargs['range_sep'] = self.range_sep
return name, path, args, kwargs 18 19 return name, path, args, kwargs
19 20
def db_type(self, connection): 20 21 def db_type(self, connection):
return 'integer[2][]' if self._using_array(connection) else 'varchar' 21 22 return 'integer[2][]' if self._using_array(connection) else 'varchar'
22 23
def from_db_value(self, value, expression, connection, context): 23 24 def from_db_value(self, value, expression, connection, context):
if value is None: 24 25 if value is None:
return value 25 26 return value
if self._using_array(connection): 26 27 if self._using_array(connection):
return MaskField._psql_parse_mask(value) 27 28 return MaskField._psql_parse_mask(value)
return MaskField._varchar_parse_mask(value) 28 29 return MaskField._varchar_parse_mask(value)
29 30
def get_db_prep_value(self, value, connection, prepared=False): 30 31 def get_db_prep_value(self, value, connection, prepared=False):
if not prepared: 31 32 if not prepared:
value = self.get_prep_value(value) 32 33 value = self.get_prep_value(value)
if value is None: 33 34 if value is None:
return value 34 35 return value
if self._using_array(connection): 35 36 if self._using_array(connection):
return value 36 37 return value
return ','.join(['-'.join(map(str, i)) for i in value]) 37 38 return ','.join(['-'.join(map(str, i)) for i in value])
38 39
def to_python(self, value): 39 40 def to_python(self, value):
if value is None or isinstance(value, set): 40 41 if value is None:
return value 41 42 return value
return MaskField._parse_mask(value) 42 43 return sorted(list(FlashcardMask(value)))
43 44
def get_prep_value(self, value): 44 45 def get_prep_value(self, value):
if value is None: 45 46 if value is None:
return value 46 47 return value
if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]): 47 48 return sorted(list(FlashcardMask(value)))
raise ValueError("Invalid value for MaskField attribute") 48
return self.__class__._parse_mask(sorted(value)) 49
50 49
def get_prep_lookup(self, lookup_type, value): 51 50 def get_prep_lookup(self, lookup_type, value):
raise TypeError("Lookup not supported for MaskField") 52 51 raise TypeError("Lookup not supported for MaskField")
53 52
@staticmethod 54 53 @staticmethod
def _parse_mask(intervals): 55 54 def _parse_mask(intervals):
p_beg, p_end = -1, -1 56 55 p_beg, p_end = -1, -1
mask_list = [] 57 56 mask_list = []
for interval in intervals: 58 57 for interval in intervals:
beg, end = map(int, interval) 59 58 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 60 59 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise ValueError("Invalid range offsets in the mask") 61 60 raise ValueError("Invalid range offsets in the mask")
mask_list.append([beg, end]) 62 61 mask_list.append([beg, end])
p_beg, p_end = beg, end 63 62 p_beg, p_end = beg, end
return mask_list 64 63 return mask_list
65 64
@classmethod 66 65 @classmethod
def _varchar_parse_mask(cls, value): 67 66 def _varchar_parse_mask(cls, value):
intervals = [] 68 67 mask = [tuple(map(int, i.split('-'))) for i in value.split(',')]
ranges = value.split(',') 69 68 return FlashcardMask(mask)
for interval in ranges: 70
_range = interval.split('-') 71
if len(_range) != 2 or not all(map(unicode.isdigit, _range)): 72
flashcards/serializers.py View file @ 7fd48b3
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from flashcards.models import Section, LecturePeriod, User, Flashcard 2 2 from flashcards.models import Section, LecturePeriod, User, Flashcard
3 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 3 4 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField 4 5 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField
from rest_framework.relations import HyperlinkedRelatedField 5 6 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 6 7 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 7 8 from rest_framework.validators import UniqueValidator
from json import dumps, loads 8 9 from json import dumps, loads
9 10
10 11
class EmailSerializer(Serializer): 11 12 class EmailSerializer(Serializer):
email = EmailField(required=True) 12 13 email = EmailField(required=True)
13 14
14 15
class EmailPasswordSerializer(EmailSerializer): 15 16 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 16 17 password = CharField(required=True)
17 18
18 19
class RegistrationSerializer(EmailPasswordSerializer): 19 20 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 20 21 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
21 22
22 23
class PasswordResetRequestSerializer(EmailSerializer): 23 24 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 24 25 def validate_email(self, value):
try: 25 26 try:
User.objects.get(email=value) 26 27 User.objects.get(email=value)
return value 27 28 return value
except User.DoesNotExist: 28 29 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 29 30 raise serializers.ValidationError('No user exists with that email')
30 31
31 32
class PasswordResetSerializer(Serializer): 32 33 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 33 34 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 34 35 uid = IntegerField(required=True)
token = CharField(required=True) 35 36 token = CharField(required=True)
36 37
def validate_uid(self, value): 37 38 def validate_uid(self, value):
try: 38 39 try:
User.objects.get(id=value) 39 40 User.objects.get(id=value)
return value 40 41 return value
except User.DoesNotExist: 41 42 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 42 43 raise serializers.ValidationError('Could not verify reset token')
43 44
44 45
class UserUpdateSerializer(Serializer): 45 46 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 46 47 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 47 48 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 48 49 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 49 50 # reset_token = CharField(required=False)
50 51
def validate(self, data): 51 52 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 52 53 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 53 54 raise serializers.ValidationError('old_password is required to set a new_password')
return data 54 55 return data
55 56
56 57
class Password(Serializer): 57 58 class Password(Serializer):
email = EmailField(required=True) 58 59 email = EmailField(required=True)
password = CharField(required=True) 59 60 password = CharField(required=True)
60 61
61 62
class LecturePeriodSerializer(ModelSerializer): 62 63 class LecturePeriodSerializer(ModelSerializer):
class Meta: 63 64 class Meta:
model = LecturePeriod 64 65 model = LecturePeriod
exclude = 'id', 'section' 65 66 exclude = 'id', 'section'
66 67
67 68
class SectionSerializer(ModelSerializer): 68 69 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 69 70 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
70 71
class Meta: 71 72 class Meta:
model = Section 72 73 model = Section
73 74
74 75
class UserSerializer(ModelSerializer): 75 76 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 76 77 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 77 78 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 78 79 is_confirmed = BooleanField()
79 80
class Meta: 80 81 class Meta:
model = User 81 82 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 82 83 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
83 84
84 85
class MaskFieldSerializer(serializers.Field): 85 86 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 86 87 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 87 88 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 88 89 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 89 90 'overlap': 'Ensure this field does not have overlapping intervals.'
} 90 91 }
91 92
def to_representation(self, value): 92 93 def to_representation(self, value):
if not isinstance(value, set) or not all([isinstance(i, tuple) for i in value]): 93 94 return dumps(list(self._make_mask(value)))
raise serializers.ValidationError("Invalid MaskField.") 94
return dumps(list(value)) 95
96 95
def to_internal_value(self, data): 97 96 def to_internal_value(self, value):
97 return self._make_mask(value)
98
99 def _make_mask(self, data):
try: 98 100 try:
intervals = loads(data) 99 101 mask = FlashcardMask(loads(data))
if not isinstance(intervals, list) or len(intervals) > 32 \ 100
or not all([isinstance(i, list) and len(i) == 2 for i in intervals]): 101
raise ValueError 102
except ValueError: 103 102 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 104 103 raise serializers.ValidationError("Invalid JSON for MaskField")
return set([tuple(i) for i in intervals]) 105 104 except TypeError:
105 raise serializers.ValidationError("Invalid data for MaskField.")
106 except OverlapIntervalException:
107 raise serializers.ValidationError("Invalid intervals for MaskField data.")
108 if len(mask) > 32:
109 raise serializers.ValidationError("Too many intervals in the mask.")
110 return mask
106 111
107 112
class FlashcardSerializer(ModelSerializer): 108 113 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 109 114 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 110 115 hide_reason = CharField(read_only=True)
mask = MaskFieldSerializer() 111 116 mask = MaskFieldSerializer()
112 117
def validate_material_date(self, value): 113 118 def validate_material_date(self, value):
# TODO: make this dynamic 114 119 # TODO: make this dynamic
quarter_start = datetime(2015, 3, 15) 115 120 quarter_start = datetime(2015, 3, 15)
quarter_end = datetime(2015, 6, 15) 116 121 quarter_end = datetime(2015, 6, 15)
if quarter_start <= value <= quarter_end: 117 122 if quarter_start <= value <= quarter_end:
return value 118 123 return value
flashcards/tests/test_models.py View file @ 7fd48b3
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard 4 4 from flashcards.models import User, Section, Flashcard
5 from flashcards.validators import OverlapIntervalException
5 6
6 7
class RegistrationTests(TestCase): 7 8 class RegistrationTests(TestCase):
def setUp(self): 8 9 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 9 10 User.objects.create_user(email="none@none.com", password="1234")
10 11
def test_email_confirmation(self): 11 12 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 12 13 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 13 14 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 14 15 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 15 16 self.assertTrue(user.is_confirmed)
16 17
17 18
class UserTests(TestCase): 18 19 class UserTests(TestCase):
def setUp(self): 19 20 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 20 21 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 21 22 Section.objects.create(department='dept',
course_num='101a', 22 23 course_num='101a',
course_title='how 2 test', 23 24 course_title='how 2 test',
instructor='George Lucas', 24 25 instructor='George Lucas',
quarter='SP15') 25 26 quarter='SP15')
26 27
def test_section_list(self): 27 28 def test_section_list(self):
section = Section.objects.get(course_num='101a') 28 29 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 29 30 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 30 31 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 31 32 user.sections.add(section)
self.assertIn(section, user.sections.all()) 32 33 self.assertIn(section, user.sections.all())
user.sections.add(section) 33 34 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 34 35 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 35 36 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 36 37 self.assertEqual(user.sections.count(), 0)
37 38
38 39
class FlashcardTests(TestCase): 39 40 class FlashcardTests(TestCase):
def setUp(self): 40 41 def setUp(self):
user = User.objects.create_user(email="none@none.com", password="1234") 41 42 user = User.objects.create_user(email="none@none.com", password="1234")
section = Section.objects.create(department='dept', 42 43 section = Section.objects.create(department='dept',
course_num='101a', 43 44 course_num='101a',
course_title='how 2 test', 44 45 course_title='how 2 test',
instructor='George Lucas', 45 46 instructor='George Lucas',
quarter='SP15') 46 47 quarter='SP15')
Flashcard.objects.create(text="This is the text of the Flashcard", 47 48 Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 48 49 section=section,
author=user, 49 50 author=user,
material_date=datetime.now(), 50 51 material_date=datetime.now(),
previous=None, 51 52 previous=None,
mask={(24,34), (0, 4)}) 52 53 mask={(24,34), (0, 4)})
53 54
def test_mask_field(self): 54 55 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 55 56 user = User.objects.get(email="none@none.com")
section = Section.objects.get(course_title='how 2 test') 56 57 section = Section.objects.get(course_title='how 2 test')
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 57 58 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 58 59 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 59 60 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 60 61 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 61 62 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 62 63 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
try: 63 64 try:
flashcards/validators.py View file @ 7fd48b3
File was created 1 __author__ = 'rray'
2
3 from collections import Iterable
4
5
6 class FlashcardMask(set):
7 def __init__(self, *args, **kwargs):
8 super(FlashcardMask, self).__init__(*args, **kwargs)
9 self._iterable_check()
10 self._interval_check()
11 self._overlap_check()
12
13 def _iterable_check(self):
14 if not all([isinstance(i, Iterable) for i in self]):
15 raise TypeError("Interval not a valid iterable")
16
17 def _interval_check(self):
18 if not all([len(i) == 2 for i in self]):
19 raise TypeError("Intervals must have exactly 2 elements, begin and end")
20
21 def _overlap_check(self):
22 p_beg, p_end = -1, -1
23 for interval in sorted(self):
24 beg, end = map(int, interval)
25 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
26 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
27 p_beg, p_end = beg, end
flashcards/views.py View file @ 7fd48b3
from django.contrib import auth 1 1 from django.contrib import auth
from django.db.models import Q 2 2 from django.db.models import Q
from flashcards.api import StandardResultsSetPagination 3 3 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 4 4 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 5 5 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 6 6 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 7 7 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 8 8 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 9 9 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 10 10 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 11 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 12 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 13 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 14 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 15 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 16 16 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 17 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 18 from simple_email_confirmation import EmailAddress
19 19
20 20
class SectionViewSet(ReadOnlyModelViewSet): 21 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 24 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 25 permission_classes = [IsAuthenticated]
26 26
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 27 27 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 28 28 def flashcards(self, request, pk):
""" 29 29 """
Gets flashcards for a section, excluding hidden cards. 30 30 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 31 31 Returned in strictly chronological order (material date).
""" 32 32 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 33 33 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object(), is_hidden=False).all() 34 34 section=self.get_object(), is_hidden=False).all()
35 35
return Response(FlashcardSerializer(flashcards, many=True)) 36 36 return Response(FlashcardSerializer(flashcards, many=True))
37 37
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 38 38 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 39 39 def enroll(self, request, pk):
""" 40 40 """
Add the current user to a specified section 41 41 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. 42 42 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 43 43 ---
omit_serializer: true 44 44 omit_serializer: true
parameters: 45 45 parameters:
- fake: None 46 46 - fake: None
parameters_strategy: 47 47 parameters_strategy:
form: replace 48 48 form: replace
""" 49 49 """
section = self.get_object() 50 50 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 51 51 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 52 52 raise ValidationError("You are already in this section.")
if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): 53 53 if section.is_whitelisted() and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 54 54 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 55 55 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 56 56 return Response(status=HTTP_204_NO_CONTENT)
57 57
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 58 58 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 59 59 def drop(self, request, pk):
""" 60 60 """
Remove the current user from a specified section 61 61 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 62 If the user is not in the class, the request will fail.
--- 63 63 ---
omit_serializer: true 64 64 omit_serializer: true
parameters: 65 65 parameters:
- fake: None 66 66 - fake: None
parameters_strategy: 67 67 parameters_strategy:
form: replace 68 68 form: replace
""" 69 69 """
section = self.get_object() 70 70 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 71 71 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 72 72 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 73 73 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 74 74 return Response(status=HTTP_204_NO_CONTENT)
75 75
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 76 76 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 77 77 def search(self, request):
query = request.GET.get('q', '').split(' ') 78 78 query = request.GET.get('q', '').split(' ')
q = Q() 79 79 q = Q()
for word in query: 80 80 for word in query:
q |= Q(course_title__icontains=word) 81 81 q |= Q(course_title__icontains=word)
qs = Section.objects.filter(q).distinct() 82 82 qs = Section.objects.filter(q).distinct()
serializer = SectionSerializer(qs, many=True) 83 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 84 84 return Response(serializer.data)
85 85
86 86
class UserSectionListView(ListAPIView): 87 87 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 88 88 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 89 89 permission_classes = [IsAuthenticated]
90 90
def get_queryset(self): 91 91 def get_queryset(self):
return self.request.user.sections.all() 92 92 return self.request.user.sections.all()
93 93
def paginate_queryset(self, queryset): return None 94 94 def paginate_queryset(self, queryset): return None
95 95
96 96
class UserDetail(GenericAPIView): 97 97 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 98 98 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 99 99 permission_classes = [IsAuthenticated]
100 100
def get_queryset(self): 101 101 def get_queryset(self):
return User.objects.all() 102 102 return User.objects.all()
103 103
def patch(self, request, format=None): 104 104 def patch(self, request, format=None):
""" 105 105 """
Updates the user's password, or verifies their email address 106 106 Updates the user's password, or verifies their email address
--- 107 107 ---
request_serializer: UserUpdateSerializer 108 108 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 109 109 response_serializer: UserSerializer
""" 110 110 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 111 111 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 112 112 data.is_valid(raise_exception=True)
data = data.validated_data 113 113 data = data.validated_data
114 114
if 'new_password' in data: 115 115 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 116 116 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 117 117 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 118 118 request.user.set_password(data['new_password'])
request.user.save() 119 119 request.user.save()
120 120
if 'confirmation_key' in data: 121 121 if 'confirmation_key' in data:
try: 122 122 try:
request.user.confirm_email(data['confirmation_key']) 123 123 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 124 124 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 125 125 raise ValidationError('confirmation_key is invalid')
126 126
return Response(UserSerializer(request.user).data) 127 127 return Response(UserSerializer(request.user).data)
128 128
def get(self, request, format=None): 129 129 def get(self, request, format=None):
""" 130 130 """
Return data about the user 131 131 Return data about the user
--- 132 132 ---
response_serializer: UserSerializer 133 133 response_serializer: UserSerializer
""" 134 134 """
serializer = UserSerializer(request.user, context={'request': request}) 135 135 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 136 136 return Response(serializer.data)
137 137
def delete(self, request): 138 138 def delete(self, request):
""" 139 139 """
Irrevocably delete the user and their data 140 140 Irrevocably delete the user and their data
141 141
Yes, really 142 142 Yes, really
""" 143 143 """
request.user.delete() 144 144 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 145 145 return Response(status=HTTP_204_NO_CONTENT)
146 146
147 147
@api_view(['POST']) 148 148 @api_view(['POST'])
def register(request, format=None): 149 149 def register(request, format=None):
""" 150 150 """
Register a new user 151 151 Register a new user
--- 152 152 ---
request_serializer: EmailPasswordSerializer 153 153 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 154 154 response_serializer: UserSerializer
""" 155 155 """
data = RegistrationSerializer(data=request.data) 156 156 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 157 157 data.is_valid(raise_exception=True)
158 158
User.objects.create_user(**data.validated_data) 159 159 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 160 160 user = authenticate(**data.validated_data)
auth.login(request, user) 161 161 auth.login(request, user)
162 162
body = ''' 163 163 body = '''
Visit the following link to confirm your email address: 164 164 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 165 165 https://flashy.cards/app/verify_email/%s
166 166
If you did not register for Flashy, no action is required. 167 167 If you did not register for Flashy, no action is required.
''' 168 168 '''
169 169
assert send_mail("Flashy email verification", 170 170 assert send_mail("Flashy email verification",
body % user.confirmation_key, 171 171 body % user.confirmation_key,
"noreply@flashy.cards", 172 172 "noreply@flashy.cards",
[user.email]) 173 173 [user.email])
174 174
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 175 175 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
176 176
177 177
@api_view(['POST']) 178 178 @api_view(['POST'])
def login(request): 179 179 def login(request):
""" 180 180 """
Authenticates user and returns user data if valid. 181 181 Authenticates user and returns user data if valid.
--- 182 182 ---
request_serializer: EmailPasswordSerializer 183 183 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 184 184 response_serializer: UserSerializer
""" 185 185 """
186 186
data = EmailPasswordSerializer(data=request.data) 187 187 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 188 188 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 189 189 user = authenticate(**data.validated_data)
190 190
if user is None: 191 191 if user is None:
raise AuthenticationFailed('Invalid email or password') 192 192 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 193 193 if not user.is_active:
raise NotAuthenticated('Account is disabled') 194 194 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 195 195 auth.login(request, user)
return Response(UserSerializer(request.user).data) 196 196 return Response(UserSerializer(request.user).data)
197 197
198 198
@api_view(['POST']) 199 199 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 200 200 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 201 201 def logout(request, format=None):
""" 202 202 """
Logs the authenticated user out. 203 203 Logs the authenticated user out.
""" 204 204 """
auth.logout(request) 205 205 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 206 206 return Response(status=HTTP_204_NO_CONTENT)
207 207
208 208
@api_view(['POST']) 209 209 @api_view(['POST'])
def request_password_reset(request, format=None): 210 210 def request_password_reset(request, format=None):
""" 211 211 """
Send a password reset token/link to the provided email. 212 212 Send a password reset token/link to the provided email.
--- 213 213 ---
request_serializer: PasswordResetRequestSerializer 214 214 request_serializer: PasswordResetRequestSerializer
""" 215 215 """
data = PasswordResetRequestSerializer(data=request.data) 216 216 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 217 217 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 218 218 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 219 219 token = default_token_generator.make_token(user)
220 220
body = ''' 221 221 body = '''
Visit the following link to reset your password: 222 222 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 223 223 https://flashy.cards/app/reset_password/%d/%s
224 224
If you did not request a password reset, no action is required. 225 225 If you did not request a password reset, no action is required.
''' 226 226 '''
227 227
send_mail("Flashy password reset", 228 228 send_mail("Flashy password reset",
body % (user.pk, token), 229 229 body % (user.pk, token),
"noreply@flashy.cards", 230 230 "noreply@flashy.cards",
[user.email]) 231 231 [user.email])
232 232
return Response(status=HTTP_204_NO_CONTENT) 233 233 return Response(status=HTTP_204_NO_CONTENT)
234 234
235 235
@api_view(['POST']) 236 236 @api_view(['POST'])
def reset_password(request, format=None): 237 237 def reset_password(request, format=None):
""" 238 238 """
Updates user's password to new password if token is valid. 239 239 Updates user's password to new password if token is valid.
--- 240 240 ---
request_serializer: PasswordResetSerializer 241 241 request_serializer: PasswordResetSerializer
""" 242 242 """
data = PasswordResetSerializer(data=request.data) 243 243 data = PasswordResetSerializer(data=request.data)