Commit 41819dd7ec2f22a04db83459c33e31c2b7d16d99

Authored by Rohan Rangray
Exists in master

Resolved merge conflicts in view.py and test_api.py

Showing 4 changed files Inline Diff

Flashy requires Python 2. Srsly 1 1 Flashy requires Python 2. Srsly
2 2
All of these commands should be run from this directory (the one containing README.md) 3 3 All of these commands should be run from this directory (the one containing README.md)
4 4
5 Virtualenv for Windows creates a dir inexplicably named scripts rather than bin. So substitute venv/bin for venv/scripts if you are on Windows.
6
Install virtualenv before continuing. This is most easily accomplished with: 5 7 Install virtualenv before continuing. This is most easily accomplished with:
6 8
pip install virtualenv 7 9 pip install virtualenv
8 10
Set up the environment by running: 9 11 Set up the environment by running:
10 12
scripts/setup.sh 11 13 scripts/setup.sh
12 14
If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv: 13 15 If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv:
14 16
. venv/bin/activate 15 17 . venv/bin/activate
16 18
flashcards/models.py View file @ 41819dd
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied 2 2 from django.core.exceptions import PermissionDenied
from django.db.models import * 3 3 from django.db.models import *
from django.utils.timezone import now 4 4 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 5 5 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 6 6 from fields import MaskField
from datetime import datetime 7 7 from datetime import datetime
8 8
# Hack to fix AbstractUser before subclassing it 9 9 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 10 10 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 11 11 AbstractUser._meta.get_field('username')._unique = False
12 12
13 13
class EmailOnlyUserManager(UserManager): 14 14 class EmailOnlyUserManager(UserManager):
""" 15 15 """
A tiny extension of Django's UserManager which correctly creates users 16 16 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 17 17 without usernames (using emails instead).
""" 18 18 """
19 19
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 20 20 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 21 21 """
Creates and saves a User with the given email and password. 22 22 Creates and saves a User with the given email and password.
""" 23 23 """
email = self.normalize_email(email) 24 24 email = self.normalize_email(email)
user = self.model(email=email, 25 25 user = self.model(email=email,
is_staff=is_staff, is_active=True, 26 26 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 27 27 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 28 28 date_joined=now(), **extra_fields)
user.set_password(password) 29 29 user.set_password(password)
user.save(using=self._db) 30 30 user.save(using=self._db)
return user 31 31 return user
32 32
def create_user(self, email, password=None, **extra_fields): 33 33 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 34 34 return self._create_user(email, password, False, False, **extra_fields)
35 35
def create_superuser(self, email, password, **extra_fields): 36 36 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 37 37 return self._create_user(email, password, True, True, **extra_fields)
38 38
39 39
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 40 40 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 41 41 """
An extension of Django's default user model. 42 42 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 43 43 We use email as the username field, and include enrolled sections here
""" 44 44 """
objects = EmailOnlyUserManager() 45 45 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 46 46 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 47 47 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 48 48 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
49 49
def is_in_section(self, section): 50 50 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 51 51 return self.sections.filter(pk=section.pk).exists()
52 52
def pull(self, flashcard): 53 53 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 54 54 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 55 55 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 56 56 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 57 57 user_card.pulled = datetime.now()
user_card.save() 58 58 user_card.save()
59 59
def get_deck(self, section): 60 60 def get_deck(self, section):
if not self.is_in_section(section): 61 61 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 62 62 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 63 63 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
64 64
65 65
class UserFlashcard(Model): 66 66 class UserFlashcard(Model):
""" 67 67 """
Represents the relationship between a user and a flashcard by: 68 68 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 69 69 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 70 70 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 71 71 3. A user has a flashcard hidden from them
""" 72 72 """
user = ForeignKey('User') 73 73 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 74 74 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 75 75 help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") 76 76 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 77 77 flashcard = ForeignKey('Flashcard')
78 78
class Meta: 79 79 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 80 80 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 81 81 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 82 82 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 83 83 # By default, order by most recently pulled
ordering = ['-pulled'] 84 84 ordering = ['-pulled']
85 85
86 86
class FlashcardHide(Model): 87 87 class FlashcardHide(Model):
""" 88 88 """
Represents the property of a flashcard being hidden by a user. 89 89 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 90 90 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 91 91 If reason is null, the flashcard was just hidden.
If reason is not null, the flashcard was reported, and reason is the reason why it was reported. 92 92 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 93 93 """
user = ForeignKey('User') 94 94 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 95 95 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 96 96 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 97 97 hidden = DateTimeField(auto_now_add=True)
98 98
class Meta: 99 99 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 100 100 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 101 101 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 102 102 index_together = ["user", "flashcard"]
103 103
104 104
class Flashcard(Model): 105 105 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 106 106 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') 107 107 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") 108 108 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(default=now, help_text="The date with which the card is associated") 109 109 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 110 110 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 111 111 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 112 112 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 113 113 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 114 114 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 115 115 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
116 116
class Meta: 117 117 class Meta:
# By default, order by most recently pushed 118 118 # By default, order by most recently pushed
ordering = ['-pushed'] 119 119 ordering = ['-pushed']
120 120
def is_hidden_from(self, user): 121 121 def is_hidden_from(self, user):
""" 122 122 """
A card can be hidden globally, but if a user has the card in their deck, 123 123 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 124 124 this visibility overrides a global hide.
:param user: 125 125 :param user:
:return: Whether the card is hidden from the user. 126 126 :return: Whether the card is hidden from the user.
""" 127 127 """
result = user.userflashcard_set.filter(flashcard=self) 128 128 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 129 129 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 130 130 return result[0].is_hidden()
131 131
def edit(self, user, new_flashcard): 132 132 def edit(self, user, new_flashcard):
""" 133 133 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 134 134 Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard.
Sets up everything correctly so this object, when saved, will result in the appropriate changes. 135 135 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 136 136 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 137 137 :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 138 138 """
if not user.is_in_section(self.section): 139 139 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to edit this card") 140 140 raise PermissionDenied("You don't have the permission to edit this card")
141 141
# content_changed is True iff either material_date or text were changed 142 142 # content_changed is True iff either material_date or text were changed
content_changed = False 143 143 content_changed = False
# create_new is True iff the user editing this card is the author of this card 144 144 # create_new is True iff the user editing this card is the author of this card
# and there are no other users with this card in their decks 145 145 # and there are no other users with this card in their decks
create_new = user != self.author or \ 146 146 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 147 147 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
148 148
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 149 149 if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
content_changed |= True 150 150 content_changed |= True
self.material_date = new_flashcard['material_date'] 151 151 self.material_date = new_flashcard['material_date']
if 'text' in new_flashcard and self.text != new_flashcard['text']: 152 152 if 'text' in new_flashcard and self.text != new_flashcard['text']:
content_changed |= True 153 153 content_changed |= True
self.text = new_flashcard['text'] 154 154 self.text = new_flashcard['text']
if create_new and content_changed: 155 155 if create_new and content_changed:
self.pk = None 156 156 self.pk = None
if 'mask' in new_flashcard: 157 157 if 'mask' in new_flashcard:
self.mask = new_flashcard['mask'] 158 158 self.mask = new_flashcard['mask']
self.save() 159 159 self.save()
160 160
@classmethod 161 161 @classmethod
def cards_visible_to(cls, user): 162 162 def cards_visible_to(cls, user):
""" 163 163 """
:param user: 164 164 :param user:
:return: A queryset with all cards that should be visible to a user. 165 165 :return: A queryset with all cards that should be visible to a user.
""" 166 166 """
return cls.objects.filter(is_hidden=False).exclude(userflashcard__user=user, userflashcard__pulled=None) 167 167 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
168 168
169 169
class UserFlashcardQuiz(Model): 170 170 class UserFlashcardQuiz(Model):
""" 171 171 """
An event of a user being quizzed on a flashcard. 172 172 An event of a user being quizzed on a flashcard.
""" 173 173 """
user_flashcard = ForeignKey(UserFlashcard) 174 174 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 175 175 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 176 176 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") 177 177 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") 178 178 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
179 179
def status(self): 180 180 def status(self):
""" 181 181 """
There are three stages of a quiz object: 182 182 There are three stages of a quiz object:
1. the user has been shown the card 183 183 1. the user has been shown the card
2. the user has answered the card 184 184 2. the user has answered the card
3. the user has self-evaluated their response's correctness 185 185 3. the user has self-evaluated their response's correctness
186 186
:return: string (evaluated, answered, viewed) 187 187 :return: string (evaluated, answered, viewed)
""" 188 188 """
if self.correct is not None: return "evaluated" 189 189 if self.correct is not None: return "evaluated"
if self.response: return "answered" 190 190 if self.response: return "answered"
return "viewed" 191 191 return "viewed"
192 192
193 193
class Section(Model): 194 194 class Section(Model):
""" 195 195 """
A UCSD course taught by an instructor during a quarter. 196 196 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 197 197 We use the term "section" to avoid collision with the builtin keyword "class"
We index gratuitously to support autofill and because this is primarily read-only 198 198 We index gratuitously to support autofill and because this is primarily read-only
""" 199 199 """
department = CharField(db_index=True, max_length=50) 200 200 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 201 201 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 202 202 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 203 203 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 204 204 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 205 205 quarter = CharField(db_index=True, max_length=4)
206 206
@classmethod 207 207 @classmethod
def search(cls, terms): 208 208 def search(cls, terms):
""" 209 209 """
Search all fields of all sections for a particular set of terms 210 210 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 211 211 A matching section must match at least one field on each term
:param terms:iterable 212 212 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 213 213 :return: Matching QuerySet ordered by department and course number
""" 214 214 """
final_q = Q() 215 215 final_q = Q()
for term in terms: 216 216 for term in terms:
q = Q(department__icontains=term) 217 217 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 218 218 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 219 219 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 220 220 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 221 221 q |= Q(instructor__icontains=term)
final_q &= q 222 222 final_q &= q
qs = cls.objects.filter(final_q) 223 223 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 224 224 # Have the database cast the course number to an integer so it will sort properly
# ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically 225 225 # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) 226 226 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 227 227 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 228 228 return qs
229 229
@property 230 230 @property
def is_whitelisted(self): 231 231 def is_whitelisted(self):
""" 232 232 """
:return: whether a whitelist exists for this section 233 233 :return: whether a whitelist exists for this section
""" 234 234 """
return self.whitelist.exists() 235 235 return self.whitelist.exists()
236 236
def is_user_on_whitelist(self, user): 237 237 def is_user_on_whitelist(self, user):
""" 238 238 """
:return: whether the user is on the waitlist for this section 239 239 :return: whether the user is on the waitlist for this section
""" 240 240 """
return self.whitelist.filter(email=user.email).exists() 241 241 return self.whitelist.filter(email=user.email).exists()
242 242
class Meta: 243 243 class Meta:
ordering = ['-course_title'] 244 244 ordering = ['-course_title']
245 245
@property 246 246 @property
def lecture_times(self): 247 247 def lecture_times(self):
flashcards/tests/test_api.py View file @ 41819dd
from django.core import mail 1 1 from django.core import mail
from flashcards.models import User, Section, Flashcard, WhitelistedAddress 2 2 from flashcards.models import User, Section, Flashcard, WhitelistedAddress
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
from django.utils.timezone import now 6 6 from django.utils.timezone import now
7 7
8 8
class LoginTests(APITestCase): 9 9 class LoginTests(APITestCase):
fixtures = ['testusers'] 10 10 fixtures = ['testusers']
11 11
def test_login(self): 12 12 def test_login(self):
url = '/api/login' 13 13 url = '/api/login'
data = {'email': 'none@none.com', 'password': '1234'} 14 14 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 15 15 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 16 16 self.assertEqual(response.status_code, HTTP_200_OK)
17 17
data = {'email': 'none@none.com', 'password': '4321'} 18 18 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 19 19 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 20 20 self.assertContains(response, 'Invalid email or password', status_code=403)
21 21
data = {'email': 'bad@none.com', 'password': '1234'} 22 22 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 23 23 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 24 24 self.assertContains(response, 'Invalid email or password', status_code=403)
25 25
data = {'password': '4321'} 26 26 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 27 27 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 28 28 self.assertContains(response, 'email', status_code=400)
29 29
data = {'email': 'none@none.com'} 30 30 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 31 31 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 32 32 self.assertContains(response, 'password', status_code=400)
33 33
user = User.objects.get(email="none@none.com") 34 34 user = User.objects.get(email="none@none.com")
user.is_active = False 35 35 user.is_active = False
user.save() 36 36 user.save()
37 37
data = {'email': 'none@none.com', 'password': '1234'} 38 38 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 39 39 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 40 40 self.assertContains(response, 'Account is disabled', status_code=403)
41 41
def test_logout(self): 42 42 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 43 43 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout') 44 44 response = self.client.post('/api/logout')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 45 45 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
46 46
# since we're not logged in, we should get a 403 response 47 47 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me', format='json') 48 48 response = self.client.get('/api/me', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 49 49 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
50 50
51 51
class PasswordResetTest(APITestCase): 52 52 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 53 53 fixtures = ['testusers']
54 54
def test_reset_password(self): 55 55 def test_reset_password(self):
# submit the request to reset the password 56 56 # submit the request to reset the password
url = '/api/request_password_reset' 57 57 url = '/api/request_password_reset'
post_data = {'email': 'none@none.com'} 58 58 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 59 59 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 60 60 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 61 61 self.assertIn('reset your password', mail.outbox[0].body)
62 62
# capture the reset token from the email 63 63 # capture the reset token from the email
capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', 64 64 capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)',
mail.outbox[0].body) 65 65 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 66 66 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 67 67 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 68 68 reset_token = capture.group(2)
69 69
# try to reset the password with the wrong reset token 70 70 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 71 71 patch_data['token'] = 'wrong_token'
url = '/api/reset_password' 72 72 url = '/api/reset_password'
response = self.client.post(url, patch_data, format='json') 73 73 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 74 74 self.assertContains(response, 'Could not verify reset token', status_code=400)
75 75
# try to reset the password with the correct token 76 76 # try to reset the password with the correct token
patch_data['token'] = reset_token 77 77 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 78 78 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 79 79 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 80 80 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 81 81 assert user.check_password(patch_data['new_password'])
82 82
83 83
class RegistrationTest(APITestCase): 84 84 class RegistrationTest(APITestCase):
def test_create_account(self): 85 85 def test_create_account(self):
url = '/api/register' 86 86 url = '/api/register'
87 87
# missing password 88 88 # missing password
data = {'email': 'none@none.com'} 89 89 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 90 90 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 91 91 self.assertContains(response, 'password', status_code=400)
92 92
# missing email 93 93 # missing email
data = {'password': '1234'} 94 94 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 95 95 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 96 96 self.assertContains(response, 'email', status_code=400)
97 97
# create a user 98 98 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 99 99 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 100 100 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 101 101 self.assertEqual(response.status_code, HTTP_201_CREATED)
102 102
# user should not be confirmed 103 103 # user should not be confirmed
user = User.objects.get(email="none@none.com") 104 104 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 105 105 self.assertFalse(user.is_confirmed)
106 106
# check that the confirmation key was sent 107 107 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 108 108 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 109 109 self.assertIn(user.confirmation_key, mail.outbox[0].body)
110 110
# log the user out 111 111 # log the user out
self.client.logout() 112 112 self.client.logout()
113 113
# log the user in with their registered credentials 114 114 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 115 115 self.client.login(email='none@none.com', password='1234')
116 116
# try activating with an invalid key 117 117 # try activating with an invalid key
118 118
url = '/api/me' 119 119 url = '/api/me'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 120 120 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 121 121 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
122 122
# try activating with the valid key 123 123 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 124 124 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 125 125 self.assertTrue(response.data['is_confirmed'])
126 126
127 127
class ProfileViewTest(APITestCase): 128 128 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 129 129 fixtures = ['testusers']
130 130
def test_get_me(self): 131 131 def test_get_me(self):
url = '/api/me' 132 132 url = '/api/me'
response = self.client.get(url, format='json') 133 133 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 134 134 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 135 135 self.assertEqual(response.status_code, 403)
136 136
self.client.login(email='none@none.com', password='1234') 137 137 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 138 138 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 139 139 self.assertEqual(response.status_code, HTTP_200_OK)
140 140
141 141
class PasswordChangeTest(APITestCase): 142 142 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 143 143 fixtures = ['testusers']
144 144
def test_change_password(self): 145 145 def test_change_password(self):
url = '/api/me' 146 146 url = '/api/me'
user = User.objects.get(email='none@none.com') 147 147 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 148 148 self.assertTrue(user.check_password('1234'))
149 149
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 150 150 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 151 151 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
152 152
self.client.login(email='none@none.com', password='1234') 153 153 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 154 154 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 155 155 self.assertContains(response, 'old_password is required', status_code=400)
156 156
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 157 157 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 158 158 self.assertContains(response, 'old_password is incorrect', status_code=400)
159 159
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 160 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 161 161 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 162 162 user = User.objects.get(email='none@none.com')
163 163
self.assertFalse(user.check_password('1234')) 164 164 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 165 165 self.assertTrue(user.check_password('4321'))
166 166
167 167
class DeleteUserTest(APITestCase): 168 168 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 169 169 fixtures = ['testusers']
170 170
def test_delete_user(self): 171 171 def test_delete_user(self):
url = '/api/me' 172 172 url = '/api/me'
user = User.objects.get(email='none@none.com') 173 173 user = User.objects.get(email='none@none.com')
174 174
self.client.login(email='none@none.com', password='1234') 175 175 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 176 176 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 177 177 self.assertFalse(User.objects.filter(email='none@none.com').exists())
178 178
179 179
class FlashcardDetailTest(APITestCase): 180 180 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 181 181 fixtures = ['testusers', 'testsections']
182 182
def setUp(self): 183 183 def setUp(self):
section = Section.objects.get(pk=1) 184 184 section = Section.objects.get(pk=1)
user = User.objects.get(email='none@none.com') 185 185 user = User.objects.get(email='none@none.com')
186 186
self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) 187 187 self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user)
self.flashcard.save() 188 188 self.flashcard.save()
189 189
def test_get_flashcard(self): 190 190 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191 191 self.client.login(email='none@none.com', password='1234')
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 192 192 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 193 193 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 194 194 self.assertEqual(response.data["text"], "jason")
195 195
196 196
class SectionViewSetTest(APITestCase): 197 197 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 198 198 fixtures = ['testusers', 'testsections']
199 199
flashcards/views.py View file @ 41819dd
from django.contrib import auth 1 1 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 2 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 3 3 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 4 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 5 5 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 6 6 FlashcardUpdateSerializer
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
from datetime import datetime 19
20 19
21 20
class SectionViewSet(ReadOnlyModelViewSet): 22 21 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 23 22 queryset = Section.objects.all()
serializer_class = SectionSerializer 24 23 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 25 24 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 26 25 permission_classes = [IsAuthenticated]
27 26
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 28 27 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 29 28 def flashcards(self, request, pk):
""" 30 29 """
Gets flashcards for a section, excluding hidden cards. 31 30 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 32 31 Returned in strictly chronological order (material date).
""" 33 32 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 34 33 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object())
section=self.get_object(), is_hidden=False).all() 35
36
return Response(FlashcardSerializer(flashcards, many=True).data) 37 34 return Response(FlashcardSerializer(flashcards, many=True).data)
38 35
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 39 36 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 40 37 def enroll(self, request, pk):
""" 41 38 """
Add the current user to a specified section 42 39 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 43 40 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 44 41 ---
omit_serializer: true 45 42 omit_serializer: true
parameters: 46 43 parameters:
- fake: None 47 44 - fake: None
parameters_strategy: 48 45 parameters_strategy:
form: replace 49 46 form: replace
""" 50 47 """
section = self.get_object() 51 48 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 52 49 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 53 50 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 54 51 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.") 55 52 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 56 53 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 57 54 return Response(status=HTTP_204_NO_CONTENT)
58 55
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 59 56 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 60 57 def drop(self, request, pk):
""" 61 58 """
Remove the current user from a specified section 62 59 Remove the current user from a specified section
If the user is not in the class, the request will fail. 63 60 If the user is not in the class, the request will fail.
--- 64 61 ---
omit_serializer: true 65 62 omit_serializer: true
parameters: 66 63 parameters:
- fake: None 67 64 - fake: None
parameters_strategy: 68 65 parameters_strategy:
form: replace 69 66 form: replace
""" 70 67 """
section = self.get_object() 71 68 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 72 69 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 73 70 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 74 71 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 75 72 return Response(status=HTTP_204_NO_CONTENT)
76 73
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 77 74 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 78 75 def search(self, request):
query = request.GET.get('q', None) 79 76 query = request.GET.get('q', None)
if not query: return Response('[]') 80 77 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 81 78 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 82 79 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 83 80 return Response(serializer.data)
84 81
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 85 82 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def deck(self, request, pk): 86 83 def deck(self, request, pk):
""" 87 84 """
Gets the contents of a user's deck for a given section. 88 85 Gets the contents of a user's deck for a given section.
""" 89 86 """
qs = request.user.get_deck(self.get_object()) 90 87 qs = request.user.get_deck(self.get_object())
88 serializer = FlashcardSerializer(qs, many=True)
89 return Response(serializer.data)
90
91 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
92 def ordered_deck(self, request, pk):
93 """
94 Get a chronological order by material_date of flashcards for a section.
95 This excludes hidden card.
96 """
97 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 91 98 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 92 99 return Response(serializer.data)
93 100
94 101
class UserSectionListView(ListAPIView): 95 102 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 96 103 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 97 104 permission_classes = [IsAuthenticated]
98 105
def get_queryset(self): 99 106 def get_queryset(self):
return self.request.user.sections.all() 100 107 return self.request.user.sections.all()
101 108
def paginate_queryset(self, queryset): return None 102 109 def paginate_queryset(self, queryset): return None
103 110
104 111
class UserDetail(GenericAPIView): 105 112 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 106 113 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 107 114 permission_classes = [IsAuthenticated]
108 115
def get_queryset(self): 109 116 def get_queryset(self):
return User.objects.all() 110 117 return User.objects.all()
111 118
def patch(self, request, format=None): 112 119 def patch(self, request, format=None):
""" 113 120 """
Updates the user's password, or verifies their email address 114 121 Updates the user's password, or verifies their email address
--- 115 122 ---
request_serializer: UserUpdateSerializer 116 123 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 117 124 response_serializer: UserSerializer
""" 118 125 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 119 126 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 120 127 data.is_valid(raise_exception=True)
data = data.validated_data 121 128 data = data.validated_data
122 129
if 'new_password' in data: 123 130 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 124 131 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 125 132 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 126 133 request.user.set_password(data['new_password'])
request.user.save() 127 134 request.user.save()
128 135
if 'confirmation_key' in data: 129 136 if 'confirmation_key' in data:
try: 130 137 try:
request.user.confirm_email(data['confirmation_key']) 131 138 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 132 139 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 133 140 raise ValidationError('confirmation_key is invalid')
134 141
return Response(UserSerializer(request.user).data) 135 142 return Response(UserSerializer(request.user).data)
136 143
def get(self, request, format=None): 137 144 def get(self, request, format=None):
""" 138 145 """
Return data about the user 139 146 Return data about the user
--- 140 147 ---
response_serializer: UserSerializer 141 148 response_serializer: UserSerializer
""" 142 149 """
serializer = UserSerializer(request.user, context={'request': request}) 143 150 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 144 151 return Response(serializer.data)
145 152
def delete(self, request): 146 153 def delete(self, request):
""" 147 154 """
Irrevocably delete the user and their data 148 155 Irrevocably delete the user and their data
149 156
Yes, really 150 157 Yes, really
""" 151 158 """
request.user.delete() 152 159 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 153 160 return Response(status=HTTP_204_NO_CONTENT)
154 161
155 162
@api_view(['POST']) 156 163 @api_view(['POST'])
def register(request, format=None): 157 164 def register(request, format=None):
""" 158 165 """
Register a new user 159 166 Register a new user
--- 160 167 ---
request_serializer: EmailPasswordSerializer 161 168 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 162 169 response_serializer: UserSerializer
""" 163 170 """
data = RegistrationSerializer(data=request.data) 164 171 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 165 172 data.is_valid(raise_exception=True)
166 173
User.objects.create_user(**data.validated_data) 167 174 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 168 175 user = authenticate(**data.validated_data)
auth.login(request, user) 169 176 auth.login(request, user)
170 177
body = ''' 171 178 body = '''
Visit the following link to confirm your email address: 172 179 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 173 180 https://flashy.cards/app/verify_email/%s
174 181
If you did not register for Flashy, no action is required. 175 182 If you did not register for Flashy, no action is required.
''' 176 183 '''
177 184
assert send_mail("Flashy email verification", 178 185 assert send_mail("Flashy email verification",
body % user.confirmation_key, 179 186 body % user.confirmation_key,
"noreply@flashy.cards", 180 187 "noreply@flashy.cards",
[user.email]) 181 188 [user.email])
182 189
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 183 190 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
184 191
185 192
@api_view(['POST']) 186 193 @api_view(['POST'])
def login(request): 187 194 def login(request):
""" 188 195 """
Authenticates user and returns user data if valid. 189 196 Authenticates user and returns user data if valid.
--- 190 197 ---
request_serializer: EmailPasswordSerializer 191 198 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 192 199 response_serializer: UserSerializer
""" 193 200 """
194 201
data = EmailPasswordSerializer(data=request.data) 195 202 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 196 203 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 197 204 user = authenticate(**data.validated_data)
198 205
if user is None: 199 206 if user is None:
raise AuthenticationFailed('Invalid email or password') 200 207 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 201 208 if not user.is_active:
raise NotAuthenticated('Account is disabled') 202 209 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 203 210 auth.login(request, user)
return Response(UserSerializer(request.user).data) 204 211 return Response(UserSerializer(request.user).data)
205 212
206 213
@api_view(['POST']) 207 214 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 208 215 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 209 216 def logout(request, format=None):
""" 210 217 """
Logs the authenticated user out. 211 218 Logs the authenticated user out.
""" 212 219 """
auth.logout(request) 213 220 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 214 221 return Response(status=HTTP_204_NO_CONTENT)
215 222
216 223
@api_view(['POST']) 217 224 @api_view(['POST'])
def request_password_reset(request, format=None): 218 225 def request_password_reset(request, format=None):
""" 219 226 """
Send a password reset token/link to the provided email. 220 227 Send a password reset token/link to the provided email.
--- 221 228 ---
request_serializer: PasswordResetRequestSerializer 222 229 request_serializer: PasswordResetRequestSerializer
""" 223 230 """
data = PasswordResetRequestSerializer(data=request.data) 224 231 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 225 232 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 226 233 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 227 234 token = default_token_generator.make_token(user)
228 235
body = ''' 229 236 body = '''
Visit the following link to reset your password: 230 237 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 231 238 https://flashy.cards/app/reset_password/%d/%s
232 239
If you did not request a password reset, no action is required. 233 240 If you did not request a password reset, no action is required.
''' 234 241 '''
235 242
send_mail("Flashy password reset", 236 243 send_mail("Flashy password reset",
body % (user.pk, token), 237 244 body % (user.pk, token),
"noreply@flashy.cards", 238 245 "noreply@flashy.cards",
[user.email]) 239 246 [user.email])
240 247
return Response(status=HTTP_204_NO_CONTENT) 241 248 return Response(status=HTTP_204_NO_CONTENT)
242 249
243 250
@api_view(['POST']) 244 251 @api_view(['POST'])
def reset_password(request, format=None): 245 252 def reset_password(request, format=None):
""" 246 253 """
Updates user's password to new password if token is valid. 247 254 Updates user's password to new password if token is valid.
--- 248 255 ---
request_serializer: PasswordResetSerializer 249 256 request_serializer: PasswordResetSerializer
""" 250 257 """
data = PasswordResetSerializer(data=request.data) 251 258 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 252 259 data.is_valid(raise_exception=True)
253 260
user = User.objects.get(id=data['uid'].value) 254 261 user = User.objects.get(id=data['uid'].value)
# Check token validity. 255 262 # Check token validity.
256 263
if default_token_generator.check_token(user, data['token'].value): 257 264 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 258 265 user.set_password(data['new_password'].value)
user.save() 259 266 user.save()
else: 260 267 else:
raise ValidationError('Could not verify reset token') 261 268 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 262 269 return Response(status=HTTP_204_NO_CONTENT)
263 270
264 271
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 265 272 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 266 273 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 267 274 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 268 275 permission_classes = [IsAuthenticated]
269 276
# Override create in CreateModelMixin 270 277 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 271 278 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 272 279 serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) 273 280 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 274 281 serializer.validated_data['author'] = request.user
self.perform_create(serializer) 275 282 self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) 276 283 headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 277 284 return Response(serializer.data, status=HTTP_201_CREATED, headers=headers)
278 285
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 279 286 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])