Commit 389089b58816cabb22097b9526fff628dd4753e1

Authored by Andrew Buss
Exists in master

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

Showing 6 changed files Inline Diff

flashcards/.api.py.swo View file @ 389089b

No preview for this file type

flashcards/models.py View file @ 389089b
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser, UserManager
from django.db.models import * 2 2 from django.db.models import *
from django.utils.timezone import now 3 3 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 4 4 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
5 5
# Hack to fix AbstractUser before subclassing it 6 6 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 7 7 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 8 8 AbstractUser._meta.get_field('username')._unique = False
9 9
10 10
class EmailOnlyUserManager(UserManager): 11 11 class EmailOnlyUserManager(UserManager):
""" 12 12 """
A tiny extension of Django's UserManager which correctly creates users 13 13 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 14 14 without usernames (using emails instead).
""" 15 15 """
16 16
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 17 17 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 18 18 """
Creates and saves a User with the given email and password. 19 19 Creates and saves a User with the given email and password.
""" 20 20 """
email = self.normalize_email(email) 21 21 email = self.normalize_email(email)
user = self.model(email=email, 22 22 user = self.model(email=email,
is_staff=is_staff, is_active=True, 23 23 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 24 24 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 25 25 date_joined=now(), **extra_fields)
user.set_password(password) 26 26 user.set_password(password)
user.save(using=self._db) 27 27 user.save(using=self._db)
return user 28 28 return user
29 29
def create_user(self, email, password=None, **extra_fields): 30 30 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 31 31 return self._create_user(email, password, False, False, **extra_fields)
32 32
def create_superuser(self, email, password, **extra_fields): 33 33 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 34 34 return self._create_user(email, password, True, True, **extra_fields)
35 35
36 36
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 37 37 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 38 38 """
An extension of Django's default user model. 39 39 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 40 40 We use email as the username field, and include enrolled sections here
""" 41 41 """
objects = EmailOnlyUserManager() 42 42 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 43 43 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 44 44 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 45 45 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
46 46
47 47
class UserFlashcard(Model): 48 48 class UserFlashcard(Model):
""" 49 49 """
Represents the relationship between a user and a flashcard by: 50 50 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 51 51 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 52 52 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 53 53 3. A user has a flashcard hidden from them
""" 54 54 """
user = ForeignKey('User') 55 55 user = ForeignKey('User')
mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") 56 56 mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask")
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") 57 57 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 58 58 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 59 59 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
60 60
class Meta: 61 61 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 62 62 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 63 63 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 64 64 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 65 65 # By default, order by most recently pulled
ordering = ['-pulled'] 66 66 ordering = ['-pulled']
67 67
def is_hidden(self): 68 68 def is_hidden(self):
""" 69 69 """
A card is hidden only if a user has not ever added it to their deck. 70 70 A card is hidden only if a user has not ever added it to their deck.
:return: Whether the flashcard is hidden from the user 71 71 :return: Whether the flashcard is hidden from the user
""" 72 72 """
return not self.pulled 73 73 return not self.pulled
74 74
def is_in_deck(self): 75 75 def is_in_deck(self):
""" 76 76 """
:return:Whether the flashcard is in the user's deck 77 77 :return:Whether the flashcard is in the user's deck
""" 78 78 """
return self.pulled and not self.unpulled 79 79 return self.pulled and not self.unpulled
80 80
81 81
class FlashcardMask(Model): 82 82 class FlashcardMask(Model):
""" 83 83 """
A serialized list of character ranges that can be blanked out during review. 84 84 A serialized list of character ranges that can be blanked out during review.
This is encoded as '13-145,150-195' 85 85 This is encoded as '13-145,150-195'
""" 86 86 """
ranges = CharField(max_length=255) 87 87 ranges = CharField(max_length=255)
88 88
89 89
class Flashcard(Model): 90 90 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 91 91 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 92 92 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") 93 93 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(help_text="The date with which the card is associated") 94 94 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 95 95 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 96 96 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 97 97 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 98 98 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 99 99 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") 100 100 mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card")
101 101
class Meta: 102 102 class Meta:
# By default, order by most recently pushed 103 103 # By default, order by most recently pushed
ordering = ['-pushed'] 104 104 ordering = ['-pushed']
105 105
def is_hidden_from(self, user): 106 106 def is_hidden_from(self, user):
""" 107 107 """
A card can be hidden globally, but if a user has the card in their deck, 108 108 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 109 109 this visibility overrides a global hide.
:param user: 110 110 :param user:
:return: Whether the card is hidden from the user. 111 111 :return: Whether the card is hidden from the user.
""" 112 112 """
result = user.userflashcard_set.filter(flashcard=self) 113 113 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 114 114 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 115 115 return result[0].is_hidden()
116 116
117 117
@classmethod 118 118 @classmethod
def cards_visible_to(cls, user): 119 119 def cards_visible_to(cls, user):
""" 120 120 """
:param user: 121 121 :param user:
:return: A queryset with all cards that should be visible to a user. 122 122 :return: A queryset with all cards that should be visible to a user.
""" 123 123 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 124 124 return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None)
125 125
126 126
class UserFlashcardReview(Model): 127 127 class UserFlashcardReview(Model):
""" 128 128 """
An event of a user reviewing a flashcard. 129 129 An event of a user reviewing a flashcard.
""" 130 130 """
user_flashcard = ForeignKey(UserFlashcard) 131 131 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 132 132 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 133 133 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") 134 134 response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 135 135 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
136 136
def status(self): 137 137 def status(self):
""" 138 138 """
There are three stages of a review object: 139 139 There are three stages of a review object:
1. the user has been shown the card 140 140 1. the user has been shown the card
2. the user has answered the card 141 141 2. the user has answered the card
3. the user has self-evaluated their response's correctness 142 142 3. the user has self-evaluated their response's correctness
143 143
:return: string (evaluated, answered, viewed) 144 144 :return: string (evaluated, answered, viewed)
""" 145 145 """
if self.correct is not None: return "evaluated" 146 146 if self.correct is not None: return "evaluated"
if self.response: return "answered" 147 147 if self.response: return "answered"
return "viewed" 148 148 return "viewed"
149 149
150 150
class Section(Model): 151 151 class Section(Model):
""" 152 152 """
A UCSD course taught by an instructor during a quarter. 153 153 A UCSD course taught by an instructor during a quarter.
Different sections taught by the same instructor in the same quarter are considered identical. 154 154 Different sections taught by the same instructor in the same quarter are considered identical.
We use the term "section" to avoid collision with the builtin keyword "class" 155 155 We use the term "section" to avoid collision with the builtin keyword "class"
""" 156 156 """
department = CharField(max_length=50) 157 157 department = CharField(max_length=50)
course_num = CharField(max_length=6) 158 158 course_num = CharField(max_length=6)
# section_id = CharField(max_length=10) 159 159 # section_id = CharField(max_length=10)
course_title = CharField(max_length=50) 160 160 course_title = CharField(max_length=50)
instructor = CharField(max_length=100) 161 161 instructor = CharField(max_length=100)
quarter = CharField(max_length=4) 162 162 quarter = CharField(max_length=4)
whitelist = ManyToManyField(User, related_name="whitelisted_sections") 163 163 whitelist = ManyToManyField(User, related_name="whitelisted_sections")
164 164
flashcards/tests/test_api.py View file @ 389089b
from django.core import mail 1 1 from django.core import mail
from flashcards.models import User 2 2 from flashcards.models import User
from rest_framework.status import HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_401_UNAUTHORIZED
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
6 6
7 7
class LoginTests(APITestCase): 8 8 class LoginTests(APITestCase):
def setUp(self): 9 9 def setUp(self):
email = "test@flashy.cards" 10 10 email = "test@flashy.cards"
User.objects.create_user(email=email, password="1234") 11 11 User.objects.create_user(email=email, password="1234")
12 12
def test_login(self): 13 13 def test_login(self):
url = '/api/login' 14 14 url = '/api/login'
data = {'email': 'test@flashy.cards', 'password': '1234'} 15 15 data = {'email': 'test@flashy.cards', 'password': '1234'}
response = self.client.post(url, data, format='json') 16 16 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 17 17 self.assertEqual(response.status_code, HTTP_200_OK)
18 18
data = {'email': 'test@flashy.cards', 'password': '54321'} 19 19 data = {'email': 'test@flashy.cards', 'password': '54321'}
response = self.client.post(url, data, format='json') 20 20 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 21 21 self.assertContains(response, 'Invalid email or password', status_code=403)
22 22
data = {'email': 'none@flashy.cards', 'password': '54321'} 23 23 data = {'email': 'none@flashy.cards', 'password': '54321'}
response = self.client.post(url, data, format='json') 24 24 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 25 25 self.assertContains(response, 'Invalid email or password', status_code=403)
26 26
data = {'password': '54321'} 27 27 data = {'password': '54321'}
response = self.client.post(url, data, format='json') 28 28 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 29 29 self.assertContains(response, 'email', status_code=400)
30 30
data = {'email': 'none@flashy.cards'} 31 31 data = {'email': 'none@flashy.cards'}
response = self.client.post(url, data, format='json') 32 32 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 33 33 self.assertContains(response, 'password', status_code=400)
34 34
user = User.objects.get(email="test@flashy.cards") 35 35 user = User.objects.get(email="test@flashy.cards")
user.is_active = False 36 36 user.is_active = False
user.save() 37 37 user.save()
38 38
data = {'email': 'test@flashy.cards', 'password': '1234'} 39 39 data = {'email': 'test@flashy.cards', 'password': '1234'}
response = self.client.post(url, data, format='json') 40 40 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 41 41 self.assertContains(response, 'Account is disabled', status_code=403)
42 42
def test_logout(self): 43 43 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 44 44 url = '/api/login'
self.client.post('/api/logout') 45 45 data = {'email': 'test@flashy.cards', 'password': '1234'}
46 response = self.client.post(url, data, format='json')
47 self.assertEqual(response.status_code, HTTP_200_OK)
46 48
49 p = self.client.post('/api/logout')
50 self.assertEqual(p.status_code, HTTP_204_NO_CONTENT)
response = self.client.get('/api/users/me', format='json') 47 51 response = self.client.get('/api/users/me', format='json')
52
# since we're not logged in, we shouldn't be able to see this 48 53 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) 49 54 self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
50 55
class PasswordResetTest(APITestCase): 51 56 class PasswordResetTest(APITestCase):
def setUp(self): 52 57 def setUp(self):
58 # create a user to test things with
email = "test@flashy.cards" 53 59 email = "test@flashy.cards"
User.objects.create_user(email=email, password="12345") 54 60 User.objects.create_user(email=email, password="12345")
55 61
def test_reset_password(self): 56 62 def test_reset_password(self):
63 # submit the request to reset the password
url = '/api/reset_password' 57 64 url = '/api/reset_password'
post_data = {'email': 'test@flashy.cards'} 58 65 post_data = {'email': 'test@flashy.cards'}
patch_data = {'new_password': '54321', 59
'uid': '', 'token': ''} 60
self.client.post(url, post_data, format='json') 61 66 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 62 67 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 63 68 self.assertIn('reset your password', mail.outbox[0].body)
64 69
70 # capture the reset token from the email
capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', 65 71 capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)',
mail.outbox[0].body) 66 72 mail.outbox[0].body)
73 patch_data = {'new_password': '54321'}
patch_data['uid'] = capture.group(1) 67 74 patch_data['uid'] = capture.group(1)
patch_data['token'] = capture.group(2) 68 75 reset_token = capture.group(2)
self.client.patch(url, patch_data, format='json') 69 76
77 # try to reset the password with the wrong reset token
78 patch_data['token'] = 'wrong_token'
79 response = self.client.patch(url, patch_data, format='json')
80 self.assertContains(response, 'Could not verify reset token', status_code=400)
81
82 # try to reset the password with the correct token
83 patch_data['token'] = reset_token
84 response = self.client.patch(url, patch_data, format='json')
85 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 70 86 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 71 87 assert user.check_password(patch_data['new_password'])
72 88
73 89
class RegistrationTest(APITestCase): 74 90 class RegistrationTest(APITestCase):
def test_create_account(self): 75 91 def test_create_account(self):
url = '/api/users/me' 76 92 url = '/api/users/me'
77 93
# missing password 78 94 # missing password
data = {'email': 'none@none.com'} 79 95 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 80 96 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 81 97 self.assertContains(response, 'password', status_code=400)
82 98
# missing email 83 99 # missing email
data = {'password': '1234'} 84 100 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 85 101 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 86 102 self.assertContains(response, 'email', status_code=400)
87 103
# create a user 88 104 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 89 105 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 90 106 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 91 107 self.assertEqual(response.status_code, HTTP_201_CREATED)
92 108
# user should not be confirmed 93 109 # user should not be confirmed
user = User.objects.get(email="none@none.com") 94 110 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 95 111 self.assertFalse(user.is_confirmed)
96 112
# check that the confirmation key was sent 97 113 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 98 114 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 99 115 self.assertIn(user.confirmation_key, mail.outbox[0].body)
100 116
# log the user out 101 117 # log the user out
self.client.logout() 102 118 self.client.logout()
103 119
# log the user in with their registered credentials 104 120 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 105 121 self.client.login(email='none@none.com', password='1234')
106 122
# try activating with an invalid key 107 123 # try activating with an invalid key
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 108 124 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 109 125 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
110 126
# try activating with the valid key 111 127 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 112 128 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 113 129 self.assertTrue(response.data['is_confirmed'])
114 130
115 131
class ProfileViewTest(APITestCase): 116 132 class ProfileViewTest(APITestCase):
def setUp(self): 117 133 def setUp(self):
email = "profileviewtest@flashy.cards" 118 134 email = "profileviewtest@flashy.cards"
User.objects.create_user(email=email, password="1234") 119 135 User.objects.create_user(email=email, password="1234")
120 136
def test_get_me(self): 121 137 def test_get_me(self):
url = '/api/users/me' 122 138 url = '/api/users/me'
response = self.client.get(url, format='json') 123 139 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 124 140 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED) 125 141 self.assertEqual(response.status_code, HTTP_401_UNAUTHORIZED)
126 142
self.client.login(email='profileviewtest@flashy.cards', password='1234') 127 143 self.client.login(email='profileviewtest@flashy.cards', password='1234')
response = self.client.get(url, format='json') 128 144 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 129 145 self.assertEqual(response.status_code, HTTP_200_OK)
130 146
131 147
class PasswordChangeTest(APITestCase): 132 148 class PasswordChangeTest(APITestCase):
def setUp(self): 133 149 def setUp(self):
email = "none@none.com" 134 150 email = "none@none.com"
User.objects.create_user(email=email, password="1234") 135 151 User.objects.create_user(email=email, password="1234")
flashcards/views.py View file @ 389089b
from flashcards.api import StandardResultsSetPagination 1 1 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User 2 2 from flashcards.models import Section, User
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 3 3 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer 4 4 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer
from rest_framework.permissions import IsAuthenticated 5 5 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet 6 6 from rest_framework.viewsets import ReadOnlyModelViewSet, ModelViewSet
from django.core.mail import send_mail 7 7 from django.core.mail import send_mail
from django.contrib.auth import authenticate, login, logout 8 8 from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.tokens import default_token_generator 9 9 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED 10 10 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_401_UNAUTHORIZED, HTTP_201_CREATED
from rest_framework.views import APIView 11 11 from rest_framework.views import APIView
from rest_framework.response import Response 12 12 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError 13 13 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError
from simple_email_confirmation import EmailAddress 14 14 from simple_email_confirmation import EmailAddress
15 from rest_framework import filters
15 16
16
class SectionViewSet(ReadOnlyModelViewSet): 17 17 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 18 18 queryset = Section.objects.all()
serializer_class = SectionSerializer 19 19 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 20 20 pagination_class = StandardResultsSetPagination
21 21
22 class UserSectionViewSet(ModelViewSet):
23 serializer_class = SectionSerializer
24 permission_classes = [IsAuthenticated]
25 def get_queryset(self):
26 return self.request.user.sections.all()
27
28 def paginate_queryset(self, queryset): return None
22 29
class UserDetail(APIView): 23 30 class UserDetail(APIView):
def patch(self, request, format=None): 24 31 def patch(self, request, format=None):
""" 25 32 """
Updates the user's password, or verifies their email address 26 33 Updates the user's password, or verifies their email address
--- 27 34 ---
request_serializer: UserUpdateSerializer 28 35 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 29 36 response_serializer: UserSerializer
""" 30 37 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 31 38 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 32 39 data.is_valid(raise_exception=True)
data = data.validated_data 33 40 data = data.validated_data
34 41
if 'new_password' in data: 35 42 if 'new_password' in data:
if not request.user.is_authenticated(): 36 43 if not request.user.is_authenticated():
raise NotAuthenticated('You must be logged in to change your password') 37 44 raise NotAuthenticated('You must be logged in to change your password')
if not request.user.check_password(data['old_password']): 38 45 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 39 46 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 40 47 request.user.set_password(data['new_password'])
request.user.save() 41 48 request.user.save()
42 49
if 'confirmation_key' in data: 43 50 if 'confirmation_key' in data:
try: 44 51 try:
request.user.confirm_email(data['confirmation_key']) 45 52 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 46 53 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 47 54 raise ValidationError('confirmation_key is invalid')
48 55
return Response(UserSerializer(request.user).data) 49 56 return Response(UserSerializer(request.user).data)
50 57
def get(self, request, format=None): 51 58 def get(self, request, format=None):
""" 52 59 """
Return data about the user 53 60 Return data about the user
--- 54 61 ---
response_serializer: UserSerializer 55 62 response_serializer: UserSerializer
""" 56 63 """
if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED) 57 64 if not request.user.is_authenticated(): return Response(status=HTTP_401_UNAUTHORIZED)
serializer = UserSerializer(request.user) 58 65 serializer = UserSerializer(request.user)
return Response(serializer.data) 59 66 return Response(serializer.data)
60 67
def post(self, request, format=None): 61 68 def post(self, request, format=None):
""" 62 69 """
Register a new user 63 70 Register a new user
--- 64 71 ---
request_serializer: EmailPasswordSerializer 65 72 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 66 73 response_serializer: UserSerializer
""" 67 74 """
data = RegistrationSerializer(data=request.data) 68 75 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 69 76 data.is_valid(raise_exception=True)
70 77
User.objects.create_user(**data.validated_data) 71 78 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 72 79 user = authenticate(**data.validated_data)
login(request, user) 73 80 login(request, user)
74 81
body = ''' 75 82 body = '''
Visit the following link to confirm your email address: 76 83 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 77 84 https://flashy.cards/app/verify_email/%s
78 85
If you did not register for Flashy, no action is required. 79 86 If you did not register for Flashy, no action is required.
''' 80 87 '''
81 88
assert send_mail("Flashy email verification", 82 89 assert send_mail("Flashy email verification",
body % user.confirmation_key, 83 90 body % user.confirmation_key,
"noreply@flashy.cards", 84 91 "noreply@flashy.cards",
[user.email]) 85 92 [user.email])
86 93
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 87 94 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
88 95
def delete(self, request): 89 96 def delete(self, request):
""" 90 97 """
Irrevocably delete the user and their data 91 98 Irrevocably delete the user and their data
92 99
Yes, really 93 100 Yes, really
""" 94 101 """
request.user.delete() 95 102 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 96 103 return Response(status=HTTP_204_NO_CONTENT)
97 104
98 105
class UserLogin(APIView): 99 106 class UserLogin(APIView):
def post(self, request): 100 107 def post(self, request):
""" 101 108 """
Authenticates user and returns user data if valid. 102 109 Authenticates user and returns user data if valid.
--- 103 110 ---
request_serializer: EmailPasswordSerializer 104 111 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 105 112 response_serializer: UserSerializer
""" 106 113 """
107 114
data = EmailPasswordSerializer(data=request.data) 108 115 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 109 116 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 110 117 user = authenticate(**data.validated_data)
111 118
if user is None: 112 119 if user is None:
raise AuthenticationFailed('Invalid email or password') 113 120 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 114 121 if not user.is_active:
raise NotAuthenticated('Account is disabled') 115 122 raise NotAuthenticated('Account is disabled')
login(request, user) 116 123 login(request, user)
return Response(UserSerializer(request.user).data) 117 124 return Response(UserSerializer(request.user).data)
118 125
119 126
class UserLogout(APIView): 120 127 class UserLogout(APIView):
permission_classes = (IsAuthenticated,) 121 128 permission_classes = (IsAuthenticated,)
def post(self, request, format=None): 122 129 def post(self, request, format=None):
""" 123 130 """
Logs the authenticated user out. 124 131 Logs the authenticated user out.
""" 125 132 """
logout(request) 126 133 logout(request)
return Response(status=HTTP_204_NO_CONTENT) 127 134 return Response(status=HTTP_204_NO_CONTENT)
128 135
129 136
class PasswordReset(APIView): 130 137 class PasswordReset(APIView):
""" 131 138 """
Allows user to reset their password. 132 139 Allows user to reset their password.
""" 133 140 """
134 141
def post(self, request, format=None): 135 142 def post(self, request, format=None):
""" 136 143 """
Send a password reset token/link to the provided email. 137 144 Send a password reset token/link to the provided email.
--- 138 145 ---
request_serializer: PasswordResetRequestSerializer 139 146 request_serializer: PasswordResetRequestSerializer
""" 140 147 """
data = PasswordResetRequestSerializer(data=request.data) 141 148 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 142 149 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 143 150 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 144 151 token = default_token_generator.make_token(user)
145 152
body = ''' 146 153 body = '''
flashy/settings.py View file @ 389089b
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 1 1 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os 2 2 import os
3 3
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 4 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 5
IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ 6 6 IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ
7 7
DEBUG = not IN_PRODUCTION 8 8 DEBUG = not IN_PRODUCTION
9 9
ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] 10 10 ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
11 11
AUTH_USER_MODEL = 'flashcards.User' 12 12 AUTH_USER_MODEL = 'flashcards.User'
13 13 REST_FRAMEWORK = {
14 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination'
15 }
INSTALLED_APPS = ( 14 16 INSTALLED_APPS = (
'simple_email_confirmation', 15 17 'simple_email_confirmation',
'flashcards', 16 18 'flashcards',
'django.contrib.admin', 17 19 'django.contrib.admin',
'django.contrib.admindocs', 18 20 'django.contrib.admindocs',
'django.contrib.auth', 19 21 'django.contrib.auth',
'django.contrib.contenttypes', 20 22 'django.contrib.contenttypes',
'django.contrib.sessions', 21 23 'django.contrib.sessions',
'django.contrib.messages', 22 24 'django.contrib.messages',
'django.contrib.staticfiles', 23 25 'django.contrib.staticfiles',
'django_ses', 24 26 'django_ses',
'rest_framework_swagger', 25 27 'rest_framework_swagger',
'rest_framework', 26 28 'rest_framework',
) 27 29 )
28 30
REST_FRAMEWORK = { 29 31 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination', 30 32 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
'PAGE_SIZE': 20 31 33 'PAGE_SIZE': 20
} 32 34 }
33 35
MIDDLEWARE_CLASSES = ( 34 36 MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 35 37 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 36 38 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 37 39 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 38 40 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 39 41 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 40 42 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 41 43 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 42 44 'django.middleware.security.SecurityMiddleware',
) 43 45 )
44 46
ROOT_URLCONF = 'flashy.urls' 45 47 ROOT_URLCONF = 'flashy.urls'
46 48
AUTHENTICATION_BACKENDS = ( 47 49 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 48 50 'django.contrib.auth.backends.ModelBackend',
) 49 51 )
50 52
TEMPLATES = [ 51 53 TEMPLATES = [
{ 52 54 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 53 55 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 54 56 'DIRS': ['templates/'],
'APP_DIRS': True, 55 57 'APP_DIRS': True,
'OPTIONS': { 56 58 'OPTIONS': {
'context_processors': [ 57 59 'context_processors': [
'django.template.context_processors.debug', 58 60 'django.template.context_processors.debug',
'django.template.context_processors.request', 59 61 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 60 62 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 61 63 'django.contrib.messages.context_processors.messages',
], 62 64 ],
}, 63 65 },
}, 64 66 },
] 65 67 ]
66 68
WSGI_APPLICATION = 'flashy.wsgi.application' 67 69 WSGI_APPLICATION = 'flashy.wsgi.application'
68 70
DATABASES = { 69 71 DATABASES = {
'default': { 70 72 'default': {
'ENGINE': 'django.db.backends.sqlite3', 71 73 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 72 74 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 73 75 }
} 74 76 }
75 77
if IN_PRODUCTION: 76 78 if IN_PRODUCTION:
DATABASES['default'] = { 77 79 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 78 80 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 79 81 'NAME': 'flashy',
'USER': 'flashy', 80 82 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 81 83 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 82 84 'HOST': 'localhost',
'PORT': '', 83 85 'PORT': '',
} 84 86 }
85 87
LANGUAGE_CODE = 'en-us' 86 88 LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles' 87 89 TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True 88 90 USE_I18N = True
USE_L10N = True 89 91 USE_L10N = True
USE_TZ = True 90 92 USE_TZ = True
91 93
STATIC_URL = '/static/' 92 94 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 93 95 STATIC_ROOT = 'static'
94 96
# Four settings just to be sure 95 97 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 96 98 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 97 99 EMAIL_HOST_USER = 'noreply@flashy.cards'
flashy/urls.py View file @ 389089b
from django.conf.urls import include, url 1 1 from django.conf.urls import include, url
from django.contrib import admin 2 2 from django.contrib import admin
from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset 3 3 from flashcards.views import SectionViewSet, UserDetail, UserLogin, UserLogout, PasswordReset, UserSectionViewSet
from rest_framework.routers import DefaultRouter 4 4 from rest_framework.routers import DefaultRouter
from flashcards.api import * 5 5 from flashcards.api import *
6 6
router = DefaultRouter() 7 7 router = DefaultRouter()
router.register(r'sections', SectionViewSet) 8 8 router.register(r'sections', SectionViewSet)
9 9
10 router.register(r'users/me/sections', UserSectionViewSet, base_name = 'usersection')
11
urlpatterns = [ 10 12 urlpatterns = [
url(r'^api/docs/', include('rest_framework_swagger.urls')), 11 13 url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api/users/me$', UserDetail.as_view()), 12 14 url(r'^api/users/me$', UserDetail.as_view()),
url(r'^api/login$', UserLogin.as_view()), 13 15 url(r'^api/login$', UserLogin.as_view()),
url(r'^api/logout$', UserLogout.as_view()), 14 16 url(r'^api/logout$', UserLogout.as_view()),
url(r'^api/reset_password$', PasswordReset.as_view()), 15 17 url(r'^api/reset_password$', PasswordReset.as_view()),
url(r'^api/', include(router.urls)), 16 18 url(r'^api/', include(router.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 17 19 url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)), 18 20 url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')) 19 21 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework'
22 ))
] 20 23 ]
21 24