Commit 54bba1fea25b9317c79e3c7f9882aa2d9b43030a

Authored by Andrew Buss
1 parent 1cc32d8b00
Exists in master

enforce enrollment on things

Showing 4 changed files with 57 additions and 25 deletions Inline Diff

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