Commit a2d8c42294272590acc7bf17309aa03bd68f2622

Authored by Andrew Buss
1 parent 033fffbf8e
Exists in master

added feed

Showing 3 changed files with 39 additions and 16 deletions Inline Diff

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