Commit 2958a1827ea8e98f73b9ba9edff502bdf62d9398

Authored by Andrew Buss
1 parent ea351c696e
Exists in master

cleaned up some docstrings; compacted views

Showing 7 changed files with 418 additions and 82 deletions Inline Diff

flashcards/api.py View file @ 2958a18
from flashcards.models import Flashcard 1 1 from flashcards.models import Flashcard
from rest_framework.pagination import PageNumberPagination 2 2 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 3 3 from rest_framework.permissions import BasePermission
4 4
5 5
6 mock_no_params = lambda x:None
7
class StandardResultsSetPagination(PageNumberPagination): 6 8 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 7 9 page_size = 40
page_size_query_param = 'page_size' 8 10 page_size_query_param = 'page_size'
max_page_size = 1000 9 11 max_page_size = 1000
10 12
11 13
class UserDetailPermissions(BasePermission): 12 14 class UserDetailPermissions(BasePermission):
""" 13 15 """
Permissions for the user detail view. Anonymous users may only POST. 14 16 Permissions for the user detail view. Anonymous users may only POST.
""" 15 17 """
16 18
def has_object_permission(self, request, view, obj): 17 19 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 18 20 if request.method == 'POST':
return True 19 21 return True
flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py View file @ 2958a18
File was created 1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5 import django.contrib.auth.models
6 import django.utils.timezone
7 from django.conf import settings
8 import django.core.validators
9 import simple_email_confirmation.models
10 import flashcards.models
11 import flashcards.fields
12
13
14 class Migration(migrations.Migration):
15
16 replaces = [(b'flashcards', '0001_initial'), (b'flashcards', '0002_auto_20150504_1327'), (b'flashcards', '0003_auto_20150504_1600'), (b'flashcards', '0004_auto_20150506_1443'), (b'flashcards', '0005_auto_20150510_1458'), (b'flashcards', '0006_auto_20150512_0042'), (b'flashcards', '0007_userflashcard_mask'), (b'flashcards', '0008_section_department_abbreviation'), (b'flashcards', '0009_auto_20150512_0318'), (b'flashcards', '0010_auto_20150513_1546'), (b'flashcards', '0011_auto_20150514_0207'), (b'flashcards', '0012_auto_20150516_0313'), (b'flashcards', '0013_auto_20150517_0402'), (b'flashcards', '0013_auto_20150516_2356'), (b'flashcards', '0014_merge'), (b'flashcards', '0015_auto_20150518_0017')]
17
18 dependencies = [
19 ('auth', '__latest__'),
20 ]
21
22 operations = [
23 migrations.CreateModel(
24 name='User',
25 fields=[
26 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
27 ('password', models.CharField(max_length=128, verbose_name='password')),
28 ('last_login', models.DateTimeField(null=True, verbose_name='last login', blank=True)),
29 ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
30 ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username')),
31 ('first_name', models.CharField(max_length=30, verbose_name='first name', blank=True)),
32 ('last_name', models.CharField(max_length=30, verbose_name='last name', blank=True)),
33 ('email', models.EmailField(unique=True, max_length=254, verbose_name='email address', blank=True)),
34 ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
35 ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
36 ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
37 ('groups', models.ManyToManyField(related_query_name='user', related_name='user_set', to=b'auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups')),
38 ],
39 options={
40 'abstract': False,
41 'verbose_name': 'user',
42 'verbose_name_plural': 'users',
43 },
44 bases=(models.Model, simple_email_confirmation.models.SimpleEmailConfirmationUserMixin),
45 managers=[
46 ('objects', django.contrib.auth.models.UserManager()),
47 ],
48 ),
49 migrations.CreateModel(
50 name='Flashcard',
51 fields=[
52 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
53 ('text', models.CharField(help_text=b'The text on the card', max_length=255)),
54 ('pushed', models.DateTimeField(help_text=b'When the card was first pushed', auto_now_add=True)),
55 ('material_date', models.DateTimeField(help_text=b'The date with which the card is associated')),
56 ('is_hidden', models.BooleanField(default=False)),
57 ('hide_reason', models.CharField(help_text=b'Reason for hiding this card', max_length=255, blank=True)),
58 ('author', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
59 ],
60 options={
61 'ordering': ['-pushed'],
62 },
63 ),
64 migrations.CreateModel(
65 name='FlashcardMask',
66 fields=[
67 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
68 ('ranges', models.CharField(max_length=255)),
69 ],
70 ),
71 migrations.CreateModel(
72 name='FlashcardReport',
73 fields=[
74 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
75 ('reason', models.CharField(max_length=255)),
76 ('flashcard', models.ForeignKey(to='flashcards.Flashcard')),
77 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
78 ],
79 ),
80 migrations.CreateModel(
81 name='LecturePeriod',
82 fields=[
83 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
84 ('week_day', models.IntegerField(help_text=b'0-indexed day of week, starting at Monday')),
85 ('start_time', models.TimeField()),
86 ('end_time', models.TimeField()),
87 ],
88 ),
89 migrations.CreateModel(
90 name='Section',
91 fields=[
92 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
93 ('department', models.CharField(max_length=50)),
94 ('course_num', models.CharField(max_length=6)),
95 ('course_title', models.CharField(max_length=50)),
96 ('instructor', models.CharField(max_length=50)),
97 ('quarter', models.CharField(max_length=4)),
98 ('whitelist', models.ManyToManyField(related_name='whitelisted_sections', to=settings.AUTH_USER_MODEL)),
99 ],
100 options={
101 'ordering': ['-quarter'],
102 },
103 ),
104 migrations.CreateModel(
105 name='UserFlashcard',
106 fields=[
107 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
108 ('pulled', models.DateTimeField(help_text=b'When the user pulled the card', null=True, blank=True)),
109 ('unpulled', models.DateTimeField(help_text=b'When the user unpulled this card', null=True, blank=True)),
110 ('flashcard', models.ForeignKey(to='flashcards.Flashcard')),
111 ('mask', models.ForeignKey(help_text=b"A mask which overrides the card's mask", to='flashcards.FlashcardMask')),
112 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
113 ],
114 options={
115 'ordering': ['-pulled'],
116 },
117 ),
118 migrations.CreateModel(
119 name='WhitelistedAddress',
120 fields=[
121 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
122 ('email', models.EmailField(max_length=254)),
123 ('section', models.ForeignKey(related_name='whitelist', to='flashcards.Section')),
124 ],
125 ),
126 migrations.AddField(
127 model_name='lectureperiod',
128 name='section',
129 field=models.ForeignKey(to='flashcards.Section'),
130 ),
131 migrations.AddField(
132 model_name='flashcard',
133 name='previous',
134 field=models.ForeignKey(blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True),
135 ),
136 migrations.AddField(
137 model_name='flashcard',
138 name='section',
139 field=models.ForeignKey(help_text=b'The section with which the card is associated', to='flashcards.Section'),
140 ),
141 migrations.AddField(
142 model_name='user',
143 name='sections',
144 field=models.ManyToManyField(to=b'flashcards.Section'),
145 ),
146 migrations.AddField(
147 model_name='user',
148 name='user_permissions',
149 field=models.ManyToManyField(related_query_name='user', related_name='user_set', to=b'auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions'),
150 ),
151 migrations.AlterUniqueTogether(
152 name='userflashcard',
153 unique_together=set([('user', 'flashcard')]),
154 ),
155 migrations.AlterIndexTogether(
156 name='userflashcard',
157 index_together=set([('user', 'flashcard')]),
158 ),
159 migrations.AlterUniqueTogether(
160 name='section',
161 unique_together=set([('department', 'course_num', 'quarter', 'instructor')]),
162 ),
163 migrations.AlterUniqueTogether(
164 name='lectureperiod',
165 unique_together=set([('section', 'start_time', 'week_day')]),
166 ),
167 migrations.AlterUniqueTogether(
168 name='flashcardreport',
169 unique_together=set([('user', 'flashcard')]),
170 ),
171 migrations.AlterModelOptions(
172 name='section',
173 options={},
174 ),
175 migrations.AlterModelManagers(
176 name='user',
177 managers=[
178 ('objects', flashcards.models.EmailOnlyUserManager()),
179 ],
180 ),
181 migrations.AlterField(
182 model_name='flashcardreport',
183 name='reason',
184 field=models.CharField(max_length=255, blank=True),
185 ),
186 migrations.AlterField(
187 model_name='lectureperiod',
188 name='week_day',
189 field=models.IntegerField(help_text=b'1-indexed day of week, starting at Sunday'),
190 ),
191 migrations.AlterField(
192 model_name='user',
193 name='sections',
194 field=models.ManyToManyField(help_text=b'The sections which the user is enrolled in', to=b'flashcards.Section'),
195 ),
196 migrations.AlterField(
197 model_name='user',
198 name='username',
199 field=models.CharField(help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=30, error_messages={'unique': 'A user with that username already exists.'}, verbose_name='username', validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')]),
200 ),
201 migrations.AlterField(
202 model_name='section',
203 name='instructor',
204 field=models.CharField(max_length=100),
205 ),
206 migrations.AlterField(
207 model_name='userflashcard',
208 name='mask',
209 field=models.ForeignKey(blank=True, to='flashcards.FlashcardMask', help_text=b"A mask which overrides the card's mask", null=True),
210 ),
211 migrations.CreateModel(
212 name='UserFlashcardQuiz',
213 fields=[
214 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
215 ('when', models.DateTimeField(auto_now=True)),
216 ('blanked_word', models.CharField(help_text=b'The character range which was blanked', max_length=8, blank=True)),
217 ('response', models.CharField(help_text=b"The user's response", max_length=255, null=True, blank=True)),
218 ('correct', models.NullBooleanField(help_text=b"The user's self-evaluation of their response")),
219 ('user_flashcard', models.ForeignKey(to='flashcards.UserFlashcard')),
220 ],
221 ),
222 migrations.AlterModelOptions(
223 name='section',
224 options={'ordering': ['-course_title']},
225 ),
226 migrations.AlterUniqueTogether(
227 name='section',
228 unique_together=set([]),
229 ),
230 migrations.RemoveField(
231 model_name='section',
232 name='whitelist',
233 ),
234 migrations.RemoveField(
235 model_name='userflashcard',
236 name='mask',
237 ),
238 migrations.DeleteModel(
239 name='FlashcardMask',
240 ),
241 migrations.AddField(
242 model_name='flashcard',
243 name='mask',
244 field=flashcards.fields.MaskField(blank_sep=b',', range_sep=b'-', max_length=255, blank=True, help_text=b'The mask on the card', null=True),
245 ),
246 migrations.AddField(
247 model_name='userflashcard',
248 name='mask',
249 field=flashcards.fields.MaskField(default=None, blank_sep=b',', range_sep=b'-', max_length=255, blank=True, help_text=b'The user-specific mask on the card', null=True),
250 ),
251 migrations.AddField(
252 model_name='section',
253 name='department_abbreviation',
254 field=models.CharField(max_length=10, db_index=True),
255 ),
256 migrations.AlterField(
257 model_name='section',
258 name='course_num',
259 field=models.CharField(max_length=6, db_index=True),
260 ),
261 migrations.AlterField(
262 model_name='section',
263 name='course_title',
264 field=models.CharField(max_length=50, db_index=True),
265 ),
266 migrations.AlterField(
267 model_name='section',
268 name='department',
269 field=models.CharField(max_length=50, db_index=True),
270 ),
271 migrations.AlterField(
272 model_name='section',
273 name='instructor',
274 field=models.CharField(max_length=100, db_index=True),
275 ),
276 migrations.AlterField(
277 model_name='section',
278 name='quarter',
279 field=models.CharField(max_length=4, db_index=True),
280 ),
281 migrations.AlterField(
282 model_name='flashcard',
283 name='material_date',
284 field=models.DateTimeField(default=django.utils.timezone.now, help_text=b'The date with which the card is associated'),
285 ),
286 migrations.AlterModelOptions(
287 name='lectureperiod',
288 options={'ordering': ['section', 'week_day']},
289 ),
290 migrations.AlterField(
291 model_name='userflashcard',
292 name='pulled',
293 field=models.DateTimeField(default=None, help_text=b'When the user pulled the card', null=True, blank=True),
294 ),
295 migrations.RemoveField(
296 model_name='userflashcard',
297 name='unpulled',
298 ),
299 migrations.CreateModel(
300 name='FlashcardHide',
301 fields=[
302 ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
303 ('reason', models.CharField(max_length=255, null=True, blank=True)),
304 ('hidden', models.DateTimeField(auto_now_add=True)),
305 ('flashcard', models.ForeignKey(to='flashcards.Flashcard')),
306 ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
307 ],
308 ),
309 migrations.AlterUniqueTogether(
310 name='flashcardreport',
311 unique_together=set([]),
312 ),
313 migrations.RemoveField(
314 model_name='flashcardreport',
315 name='flashcard',
316 ),
317 migrations.RemoveField(
318 model_name='flashcardreport',
319 name='user',
320 ),
321 migrations.DeleteModel(
322 name='FlashcardReport',
323 ),
324 migrations.AlterUniqueTogether(
325 name='flashcardhide',
326 unique_together=set([('user', 'flashcard')]),
327 ),
328 migrations.AlterIndexTogether(
329 name='flashcardhide',
330 index_together=set([('user', 'flashcard')]),
331 ),
332 migrations.AlterField(
333 model_name='flashcard',
334 name='hide_reason',
335 field=models.CharField(default=None, help_text=b'Reason for hiding this card', max_length=255, blank=True),
336 ),
337 migrations.AlterField(
338 model_name='flashcard',
339 name='previous',
340 field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True),
341 ),
342 migrations.AlterField(
343 model_name='flashcard',
344 name='hide_reason',
345 field=models.CharField(default=None, max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True),
346 ),
347 migrations.AlterField(
348 model_name='flashcard',
349 name='previous',
350 field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True),
351 ),
flashcards/migrations/0015_auto_20150518_0017.py View file @ 2958a18
File was created 1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('flashcards', '0014_merge'),
11 ]
12
13 operations = [
14 migrations.AlterField(
flashcards/models.py View file @ 2958a18
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import ValidationError 2 2 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 3 3 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db import IntegrityError 4 4 from django.db import IntegrityError
from django.db.models import * 5 5 from django.db.models import *
from django.utils.timezone import now 6 6 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 7 7 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 8 8 from fields import MaskField
9 9
10 10
# Hack to fix AbstractUser before subclassing it 11 11 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 12 12 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 13 13 AbstractUser._meta.get_field('username')._unique = False
14 14
15
class EmailOnlyUserManager(UserManager): 16 15 class EmailOnlyUserManager(UserManager):
""" 17 16 """
A tiny extension of Django's UserManager which correctly creates users 18 17 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 19 18 without usernames (using emails instead).
""" 20 19 """
21 20
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 22 21 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 23 22 """
Creates and saves a User with the given email and password. 24 23 Creates and saves a User with the given email and password.
""" 25 24 """
email = self.normalize_email(email) 26 25 email = self.normalize_email(email)
user = self.model(email=email, 27 26 user = self.model(email=email,
is_staff=is_staff, is_active=True, 28 27 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 29 28 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 30 29 date_joined=now(), **extra_fields)
user.set_password(password) 31 30 user.set_password(password)
user.save(using=self._db) 32 31 user.save(using=self._db)
return user 33 32 return user
34 33
def create_user(self, email, password=None, **extra_fields): 35 34 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 36 35 return self._create_user(email, password, False, False, **extra_fields)
37 36
def create_superuser(self, email, password, **extra_fields): 38 37 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 39 38 return self._create_user(email, password, True, True, **extra_fields)
40 39
41 40
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 42 41 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 43 42 """
An extension of Django's default user model. 44 43 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 45 44 We use email as the username field, and include enrolled sections here
""" 46 45 """
objects = EmailOnlyUserManager() 47 46 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 48 47 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 49 48 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 50 49 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
51 50
def is_in_section(self, section): 52 51 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 53 52 return self.sections.filter(pk=section.pk).exists()
54 53
def pull(self, flashcard): 55 54 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 56 55 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 57 56 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 58 57 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 59 58 user_card.pulled = now()
user_card.save() 60 59 user_card.save()
61 60
def unpull(self, flashcard): 62 61 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 63 62 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 64 63 raise ValueError("User not in the section this flashcard belongs to")
65 64
try: 66 65 try:
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 67 66 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
except UserFlashcard.DoesNotExist: 68 67 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 69 68 raise ValueError('Cannot unpull card that is not pulled.')
70 69
user_card.delete() 71 70 user_card.delete()
72 71
def get_deck(self, section): 73 72 def get_deck(self, section):
if not self.is_in_section(section): 74 73 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 75 74 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 76 75 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
77 76
78 77
class UserFlashcard(Model): 79 78 class UserFlashcard(Model):
""" 80 79 """
Represents the relationship between a user and a flashcard by: 81 80 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 82 81 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 83 82 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 84 83 3. A user has a flashcard hidden from them
""" 85 84 """
user = ForeignKey('User') 86 85 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 87 86 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 88 87 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") 89 88 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 90 89 flashcard = ForeignKey('Flashcard')
91 90
class Meta: 92 91 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 93 92 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 94 93 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 95 94 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 96 95 # By default, order by most recently pulled
ordering = ['-pulled'] 97 96 ordering = ['-pulled']
98 97
99 98
class FlashcardHide(Model): 100 99 class FlashcardHide(Model):
""" 101 100 """
Represents the property of a flashcard being hidden by a user. 102 101 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 103 102 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 104 103 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. 105 104 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 106 105 """
user = ForeignKey('User') 107 106 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 108 107 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 109 108 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 110 109 hidden = DateTimeField(auto_now_add=True)
111 110
class Meta: 112 111 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 113 112 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 114 113 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 115 114 index_together = ["user", "flashcard"]
116 115
117 116
class Flashcard(Model): 118 117 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 119 118 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') 120 119 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") 121 120 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") 122 121 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 123 122 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 124 123 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 125 124 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 126 125 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 127 126 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 128 127 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 129 128 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
130 129
class Meta: 131 130 class Meta:
# By default, order by most recently pushed 132 131 # By default, order by most recently pushed
ordering = ['-pushed'] 133 132 ordering = ['-pushed']
134 133
def is_hidden_from(self, user): 135 134 def is_hidden_from(self, user):
""" 136 135 """
A card can be hidden globally, but if a user has the card in their deck, 137 136 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 138 137 this visibility overrides a global hide.
:param user: 139 138 :param user:
:return: Whether the card is hidden from the user. 140 139 :return: Whether the card is hidden from the user.
""" 141 140 """
if self.userflashcard_set.filter(user=user).exists(): return False 142 141 if self.userflashcard_set.filter(user=user).exists(): return False
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 143 142 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return False 144 143 return False
145 144
146 145 def hide_from(self, user, reason=None):
def hide_from(self, user): 147
if self.is_in_deck(user): user.unpull(self) 148 146 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 149 147 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 150 148 if not created:
raise ValidationError("The card has already been hidden.") 151 149 raise ValidationError("The card has already been hidden.")
obj.save() 152 150 obj.save()
153 151
def is_in_deck(self, user): 154 152 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 155 153 return self.userflashcard_set.filter(user=user).exists()
156 154
def add_to_deck(self, user): 157 155 def add_to_deck(self, user):
if not user.is_in_section(self.section): 158 156 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 159 157 raise PermissionDenied("You don't have the permission to add this card")
try: 160 158 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 161 159 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 162 160 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 163 161 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 164 162 user_flashcard.save()
return user_flashcard 165 163 return user_flashcard
166 164
def edit(self, user, new_data): 167 165 def edit(self, user, new_data):
""" 168 166 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 169 167 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. 170 168 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 171 169 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 172 170 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 173 171 """
174 172
# content_changed is True iff either material_date or text were changed 175 173 # content_changed is True iff either material_date or text were changed
content_changed = False 176 174 content_changed = False
# create_new is True iff the user editing this card is the author of this card 177 175 # 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 178 176 # and there are no other users with this card in their decks
create_new = user != self.author or \ 179 177 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 180 178 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
181 179
if 'material_date' in new_data and self.material_date != new_data['material_date']: 182 180 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 183 181 content_changed = True
self.material_date = new_data['material_date'] 184 182 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 185 183 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 186 184 content_changed = True
self.text = new_data['text'] 187 185 self.text = new_data['text']
if create_new and content_changed: 188 186 if create_new and content_changed:
self.previous_id = self.pk 189 187 self.previous_id = self.pk
if 'mask' in new_data: 190 188 if 'mask' in new_data:
self.mask = new_data['mask'] 191 189 self.mask = new_data['mask']
self.pk = None 192 190 self.pk = None
self.save() 193 191 self.save()
return create_new and content_changed 194 192 return create_new and content_changed
195 193
@classmethod 196 194 @classmethod
def cards_visible_to(cls, user): 197 195 def cards_visible_to(cls, user):
""" 198 196 """
:param user: 199 197 :param user:
:return: A queryset with all cards that should be visible to a user. 200 198 :return: A queryset with all cards that should be visible to a user.
""" 201 199 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 202 200 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
203 201
204 202
class UserFlashcardQuiz(Model): 205 203 class UserFlashcardQuiz(Model):
""" 206 204 """
An event of a user being quizzed on a flashcard. 207 205 An event of a user being quizzed on a flashcard.
""" 208 206 """
user_flashcard = ForeignKey(UserFlashcard) 209 207 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 210 208 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 211 209 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") 212 210 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") 213 211 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
214 212
def status(self): 215 213 def status(self):
""" 216 214 """
There are three stages of a quiz object: 217 215 There are three stages of a quiz object:
1. the user has been shown the card 218 216 1. the user has been shown the card
2. the user has answered the card 219 217 2. the user has answered the card
3. the user has self-evaluated their response's correctness 220 218 3. the user has self-evaluated their response's correctness
221 219
:return: string (evaluated, answered, viewed) 222 220 :return: string (evaluated, answered, viewed)
""" 223 221 """
if self.correct is not None: return "evaluated" 224 222 if self.correct is not None: return "evaluated"
if self.response: return "answered" 225 223 if self.response: return "answered"
return "viewed" 226 224 return "viewed"
227 225
228 226
class Section(Model): 229 227 class Section(Model):
""" 230 228 """
A UCSD course taught by an instructor during a quarter. 231 229 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 232 230 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 233 231 We index gratuitously to support autofill and because this is primarily read-only
""" 234 232 """
department = CharField(db_index=True, max_length=50) 235 233 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 236 234 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 237 235 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 238 236 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 239 237 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 240 238 quarter = CharField(db_index=True, max_length=4)
241 239
@classmethod 242 240 @classmethod
def search(cls, terms): 243 241 def search(cls, terms):
""" 244 242 """
Search all fields of all sections for a particular set of terms 245 243 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 246 244 A matching section must match at least one field on each term
:param terms:iterable 247 245 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 248 246 :return: Matching QuerySet ordered by department and course number
""" 249 247 """
final_q = Q() 250 248 final_q = Q()
for term in terms: 251 249 for term in terms:
q = Q(department__icontains=term) 252 250 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 253 251 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 254 252 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 255 253 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 256 254 q |= Q(instructor__icontains=term)
final_q &= q 257 255 final_q &= q
qs = cls.objects.filter(final_q) 258 256 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 259 257 # 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 260 258 # 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)"}) 261 259 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 262 260 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 263 261 return qs
264 262
@property 265 263 @property
def is_whitelisted(self): 266 264 def is_whitelisted(self):
""" 267 265 """
:return: whether a whitelist exists for this section 268 266 :return: whether a whitelist exists for this section
""" 269 267 """
return self.whitelist.exists() 270 268 return self.whitelist.exists()
271 269
def is_user_on_whitelist(self, user): 272 270 def is_user_on_whitelist(self, user):
""" 273 271 """
:return: whether the user is on the waitlist for this section 274 272 :return: whether the user is on the waitlist for this section
""" 275 273 """
return self.whitelist.filter(email=user.email).exists() 276 274 return self.whitelist.filter(email=user.email).exists()
277 275
278 276
def enroll(self, user): 279 277 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 280 278 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 281 279 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 282 280 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 283 281 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 284 282 self.user_set.add(user)
285 283
def drop(self, user): 286 284 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 287 285 if not user.sections.filter(pk=self.pk).exists():
raise ValidationError("User is not enrolled in the section.") 288 286 raise ValidationError("User is not enrolled in the section.")
self.user_set.remove(user) 289 287 self.user_set.remove(user)
290 288
class Meta: 291 289 class Meta:
ordering = ['-course_title'] 292 290 ordering = ['-course_title']
flashcards/serializers.py View file @ 2958a18
1 from json import dumps, loads
2
from django.utils.datetime_safe import datetime 1 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 2 4 from django.utils.timezone import now
import pytz 3 5 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard 4 6 from flashcards.models import Section, LecturePeriod, User, Flashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 6 8 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 7 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
from rest_framework.relations import HyperlinkedRelatedField 8 10 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 9 11 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 10 12 from rest_framework.validators import UniqueValidator
from json import dumps, loads 11
12 13
13 14
class EmailSerializer(Serializer): 14 15 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 16 email = EmailField(required=True)
16 17
17 18
class EmailPasswordSerializer(EmailSerializer): 18 19 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 20 password = CharField(required=True)
20 21
21 22
class RegistrationSerializer(EmailPasswordSerializer): 22 23 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 24 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 25
25 26
class PasswordResetRequestSerializer(EmailSerializer): 26 27 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 28 def validate_email(self, value):
try: 28 29 try:
User.objects.get(email=value) 29 30 User.objects.get(email=value)
return value 30 31 return value
except User.DoesNotExist: 31 32 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 33 raise serializers.ValidationError('No user exists with that email')
33 34
34 35
class PasswordResetSerializer(Serializer): 35 36 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 37 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 38 uid = IntegerField(required=True)
token = CharField(required=True) 38 39 token = CharField(required=True)
39 40
def validate_uid(self, value): 40 41 def validate_uid(self, value):
try: 41 42 try:
User.objects.get(id=value) 42 43 User.objects.get(id=value)
return value 43 44 return value
except User.DoesNotExist: 44 45 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 46 raise serializers.ValidationError('Could not verify reset token')
46 47
47 48
class UserUpdateSerializer(Serializer): 48 49 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 50 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 51 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 52 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 53 # reset_token = CharField(required=False)
53 54
def validate(self, data): 54 55 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 56 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 57 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 58 return data
58 59
59 60
class Password(Serializer): 60 61 class Password(Serializer):
email = EmailField(required=True) 61 62 email = EmailField(required=True)
password = CharField(required=True) 62 63 password = CharField(required=True)
63 64
64 65
class LecturePeriodSerializer(ModelSerializer): 65 66 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 67 class Meta:
model = LecturePeriod 67 68 model = LecturePeriod
exclude = 'id', 'section' 68 69 exclude = 'id', 'section'
69 70
70 71
class SectionSerializer(ModelSerializer): 71 72 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 72 73 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
lecture_times = CharField() 73 74 lecture_times = CharField()
short_name = CharField() 74 75 short_name = CharField()
long_name = CharField() 75 76 long_name = CharField()
76 77
class Meta: 77 78 class Meta:
model = Section 78 79 model = Section
79 80
80 81
class UserSerializer(ModelSerializer): 81 82 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 82 83 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 83 84 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 84 85 is_confirmed = BooleanField()
85 86
class Meta: 86 87 class Meta:
model = User 87 88 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 88 89 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
89 90
90 91
class MaskFieldSerializer(serializers.Field): 91 92 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 92 93 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 93 94 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 94 95 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 95 96 'overlap': 'Ensure this field does not have overlapping intervals.'
} 96 97 }
97 98
def to_representation(self, value): 98 99 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 99 100 return dumps(list(self._make_mask(value)))
100 101
def to_internal_value(self, value): 101 102 def to_internal_value(self, value):
return self._make_mask(loads(value)) 102 103 return self._make_mask(loads(value))
103 104
def _make_mask(self, data): 104 105 def _make_mask(self, data):
try: 105 106 try:
mask = FlashcardMask(data) 106 107 mask = FlashcardMask(data)
except ValueError: 107 108 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 108 109 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 109 110 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 110 111 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 111 112 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 112 113 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 113 114 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 114 115 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 115 116 return mask
116 117
117 118
class FlashcardSerializer(ModelSerializer): 118 119 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 119 120 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 120 121 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 121 122 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 122 123 mask = MaskFieldSerializer(allow_null=True)
123 124
def validate_material_date(self, value): 124 125 def validate_material_date(self, value):
utc = pytz.UTC 125 126 utc = pytz.UTC
# TODO: make this dynamic 126 127 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 127 128 quarter_start = utc.localize(datetime(2015, 3, 15))
quarter_end = utc.localize(datetime(2015, 6, 15)) 128 129 quarter_end = utc.localize(datetime(2015, 6, 15))
129 130
if quarter_start <= value <= quarter_end: 130 131 if quarter_start <= value <= quarter_end:
return value 131 132 return value
else: 132 133 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 133 134 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
134 135
def validate_previous(self, value): 135
if value is None: 136
return value 137
if Flashcard.objects.filter(pk=value.pk).count() > 0: 138
return value 139
raise serializers.ValidationError("Invalid previous Flashcard object") 140
141
def validate_pushed(self, value): 142 136 def validate_pushed(self, value):
flashcards/tests/test_api.py View file @ 2958a18
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 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
import datetime 6 6 import datetime
from django.utils.timezone import now 7 7 from django.utils.timezone import now
from flashcards.validators import FlashcardMask 8 8 from flashcards.validators import FlashcardMask
from flashcards.serializers import FlashcardSerializer 9 9 from flashcards.serializers import FlashcardSerializer
10 10
11 11
class LoginTests(APITestCase): 12 12 class LoginTests(APITestCase):
fixtures = ['testusers'] 13 13 fixtures = ['testusers']
14 14
def test_login(self): 15 15 def test_login(self):
url = '/api/login/' 16 16 url = '/api/login/'
data = {'email': 'none@none.com', 'password': '1234'} 17 17 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 18 18 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 19 19 self.assertEqual(response.status_code, HTTP_200_OK)
20 20
data = {'email': 'none@none.com', 'password': '4321'} 21 21 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 22 22 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 23 23 self.assertContains(response, 'Invalid email or password', status_code=403)
24 24
data = {'email': 'bad@none.com', 'password': '1234'} 25 25 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 26 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 27 27 self.assertContains(response, 'Invalid email or password', status_code=403)
28 28
data = {'password': '4321'} 29 29 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 30 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 31 31 self.assertContains(response, 'email', status_code=400)
32 32
data = {'email': 'none@none.com'} 33 33 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 34 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 35 35 self.assertContains(response, 'password', status_code=400)
36 36
user = User.objects.get(email="none@none.com") 37 37 user = User.objects.get(email="none@none.com")
user.is_active = False 38 38 user.is_active = False
user.save() 39 39 user.save()
40 40
data = {'email': 'none@none.com', 'password': '1234'} 41 41 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 42 42 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 43 43 self.assertContains(response, 'Account is disabled', status_code=403)
44 44
def test_logout(self): 45 45 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 46 46 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout/') 47 47 response = self.client.post('/api/logout/')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 48 48 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
49 49
# since we're not logged in, we should get a 403 response 50 50 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me/', format='json') 51 51 response = self.client.get('/api/me/', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 52 52 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
53 53
54 54
class PasswordResetTest(APITestCase): 55 55 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 56 56 fixtures = ['testusers']
57 57
def test_reset_password(self): 58 58 def test_reset_password(self):
# submit the request to reset the password 59 59 # submit the request to reset the password
url = '/api/request_password_reset/' 60 60 url = '/api/request_password_reset/'
post_data = {'email': 'none@none.com'} 61 61 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 62 62 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 63 63 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 64 64 self.assertIn('reset your password', mail.outbox[0].body)
65 65
# capture the reset token from the email 66 66 # capture the reset token from the email
capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', 67 67 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 68 68 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 69 69 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 70 70 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 71 71 reset_token = capture.group(2)
72 72
# try to reset the password with the wrong reset token 73 73 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 74 74 patch_data['token'] = 'wrong_token'
url = '/api/reset_password/' 75 75 url = '/api/reset_password/'
response = self.client.post(url, patch_data, format='json') 76 76 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 77 77 self.assertContains(response, 'Could not verify reset token', status_code=400)
78 78
# try to reset the password with the correct token 79 79 # try to reset the password with the correct token
patch_data['token'] = reset_token 80 80 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 81 81 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 82 82 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 83 83 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 84 84 assert user.check_password(patch_data['new_password'])
85 85
86 86
class RegistrationTest(APITestCase): 87 87 class RegistrationTest(APITestCase):
def test_create_account(self): 88 88 def test_create_account(self):
url = '/api/register/' 89 89 url = '/api/register/'
90 90
# missing password 91 91 # missing password
data = {'email': 'none@none.com'} 92 92 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 93 93 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 94 94 self.assertContains(response, 'password', status_code=400)
95 95
# missing email 96 96 # missing email
data = {'password': '1234'} 97 97 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 98 98 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 99 99 self.assertContains(response, 'email', status_code=400)
100 100
# create a user 101 101 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 102 102 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 103 103 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 104 104 self.assertEqual(response.status_code, HTTP_201_CREATED)
105 105
# user should not be confirmed 106 106 # user should not be confirmed
user = User.objects.get(email="none@none.com") 107 107 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 108 108 self.assertFalse(user.is_confirmed)
109 109
# check that the confirmation key was sent 110 110 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 111 111 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 112 112 self.assertIn(user.confirmation_key, mail.outbox[0].body)
113 113
# log the user out 114 114 # log the user out
self.client.logout() 115 115 self.client.logout()
116 116
# log the user in with their registered credentials 117 117 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 118 118 self.client.login(email='none@none.com', password='1234')
119 119
# try activating with an invalid key 120 120 # try activating with an invalid key
121 121
url = '/api/me/' 122 122 url = '/api/me/'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 123 123 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 124 124 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
125 125
# try activating with the valid key 126 126 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 127 127 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 128 128 self.assertTrue(response.data['is_confirmed'])
129 129
130 130
class ProfileViewTest(APITestCase): 131 131 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 132 132 fixtures = ['testusers']
133 133
def test_get_me(self): 134 134 def test_get_me(self):
url = '/api/me/' 135 135 url = '/api/me/'
response = self.client.get(url, format='json') 136 136 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 137 137 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 138 138 self.assertEqual(response.status_code, 403)
139 139
self.client.login(email='none@none.com', password='1234') 140 140 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 141 141 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 142 142 self.assertEqual(response.status_code, HTTP_200_OK)
143 143
144 144
class UserSectionsTest(APITestCase): 145 145 class UserSectionsTest(APITestCase):
fixtures = ['testusers', 'testsections'] 146 146 fixtures = ['testusers', 'testsections']
147 147
def setUp(self): 148 148 def setUp(self):
self.user = User.objects.get(pk=1) 149 149 self.user = User.objects.get(pk=1)
self.client.login(email='none@none.com', password='1234') 150 150 self.client.login(email='none@none.com', password='1234')
self.section = Section.objects.get(pk=1) 151 151 self.section = Section.objects.get(pk=1)
self.section.enroll(self.user) 152 152 self.section.enroll(self.user)
153 153
def test_get_user_sections(self): 154 154 def test_get_user_sections(self):
response = self.client.get('/api/me/sections/', format='json') 155 155 response = self.client.get('/api/me/sections/', format='json')
self.assertEqual(response.status_code, 200) 156 156 self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Goldstein') 157 157 self.assertContains(response, 'Goldstein')
158 158
159 159
class PasswordChangeTest(APITestCase): 160 160 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 161 161 fixtures = ['testusers']
162 162
def test_change_password(self): 163 163 def test_change_password(self):
url = '/api/me/' 164 164 url = '/api/me/'
user = User.objects.get(email='none@none.com') 165 165 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 166 166 self.assertTrue(user.check_password('1234'))
167 167
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 168 168 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 169 169 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
170 170
self.client.login(email='none@none.com', password='1234') 171 171 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 172 172 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 173 173 self.assertContains(response, 'old_password is required', status_code=400)
174 174
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 175 175 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 176 176 self.assertContains(response, 'old_password is incorrect', status_code=400)
177 177
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 178 178 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 179 179 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 180 180 user = User.objects.get(email='none@none.com')
181 181
self.assertFalse(user.check_password('1234')) 182 182 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 183 183 self.assertTrue(user.check_password('4321'))
184 184
185 185
class DeleteUserTest(APITestCase): 186 186 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 187 187 fixtures = ['testusers']
188 188
def test_delete_user(self): 189 189 def test_delete_user(self):
url = '/api/me/' 190 190 url = '/api/me/'
user = User.objects.get(email='none@none.com') 191 191 user = User.objects.get(email='none@none.com')
192 192
self.client.login(email='none@none.com', password='1234') 193 193 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 194 194 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 195 195 self.assertFalse(User.objects.filter(email='none@none.com').exists())
196 196
197 197
class FlashcardDetailTest(APITestCase): 198 198 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 199 199 fixtures = ['testusers', 'testsections']
200 200
def setUp(self): 201 201 def setUp(self):
self.section = Section.objects.get(pk=1) 202 202 self.section = Section.objects.get(pk=1)
self.user = User.objects.get(email='none@none.com') 203 203 self.user = User.objects.get(email='none@none.com')
self.section.enroll(self.user) 204 204 self.section.enroll(self.user)
self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2), 205 205 self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2),
author=self.user) 206 206 author=self.user)
self.inaccessible_flashcard.save() 207 207 self.inaccessible_flashcard.save()
self.flashcard = Flashcard(text="jason", section=self.section, author=self.user) 208 208 self.flashcard = Flashcard(text="jason", section=self.section, author=self.user)
self.flashcard.save() 209 209 self.flashcard.save()
self.flashcard.add_to_deck(self.user) 210 210 #self.flashcard.add_to_deck(self.user)
self.client.login(email='none@none.com', password='1234') 211 211 self.client.login(email='none@none.com', password='1234')
212 212
def test_edit_flashcard(self): 213 213 def test_edit_flashcard(self):
user = self.user 214 214 user = self.user
flashcard = self.flashcard 215 215 flashcard = self.flashcard
url = "/api/flashcards/{}/".format(flashcard.pk) 216 216 url = "/api/flashcards/{}/".format(flashcard.pk)
data = {'text': 'new wow for the flashcard', 217 217 data = {'text': 'new wow for the flashcard',
'mask': '[[0,4]]'} 218 218 'mask': '[[0,4]]'}
self.assertNotEqual(flashcard.text, data['text']) 219 219 self.assertNotEqual(flashcard.text, data['text'])
response = self.client.patch(url, data, format='json') 220 220 response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 221 221 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data['text'], data['text']) 222 222 self.assertEqual(response.data['text'], data['text'])
data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2), 223 223 data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2),
'mask': '[[1, 3]]'} 224 224 'mask': '[[1, 3]]'}
user2 = User.objects.create(email='wow@wow.wow', password='wow') 225 225 user2 = User.objects.create(email='wow@wow.wow', password='wow')
user2.sections.add(self.section) 226 226 user2.sections.add(self.section)
user2.save() 227 227 user2.save()
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 228 228 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
response = self.client.patch(url, data, format='json') 229 229 response = self.client.patch(url, data, format='json')
serializer = FlashcardSerializer(data=response.data) 230 230 serializer = FlashcardSerializer(data=response.data)
serializer.is_valid(raise_exception=True) 231 231 serializer.is_valid(raise_exception=True)
self.assertEqual(response.status_code, HTTP_200_OK) 232 232 self.assertEqual(response.status_code, HTTP_200_OK)
# self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date'])) 233 233 # self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date']))
self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]])) 234 234 self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]]))
data = {'mask': '[[3,6]]'} 235 235 data = {'mask': '[[3,6]]'}
response = self.client.patch(url, data, format='json') 236 236 response = self.client.patch(url, data, format='json')
user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard) 237 237 user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard)
self.assertEqual(response.status_code, HTTP_200_OK) 238 238 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]])) 239 239 self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]]))
240 240
def test_create_flashcard(self): 241 241 def test_create_flashcard(self):
data = {'text': 'this is a flashcard', 242 242 data = {'text': 'this is a flashcard',
'material_date': now(), 243 243 'material_date': now(),
'mask': '[]', 244 244 'mask': '[]',
'section': '1', 245 245 'section': '1',
'previous': None} 246 246 'previous': None}
response = self.client.post("/api/flashcards/", data, format="json") 247 247 response = self.client.post("/api/flashcards/", data, format="json")
self.assertEqual(response.status_code, HTTP_201_CREATED) 248 248 self.assertEqual(response.status_code, HTTP_201_CREATED)
self.assertEqual(response.data['text'], data['text']) 249 249 self.assertEqual(response.data['text'], data['text'])
self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists()) 250 250 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
251 251
def test_get_flashcard(self): 252 252 def test_get_flashcard(self):
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 253 253 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 254 254 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 255 255 self.assertEqual(response.data["text"], "jason")
256 256
def test_hide_flashcard(self): 257 257 def test_hide_flashcard(self):
response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json') 258 258 response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 259 259 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.flashcard.is_hidden_from(self.user)) 260 260 self.assertTrue(self.flashcard.is_hidden_from(self.user))
261
response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json') 262
self.assertContains(response, 'The card has already been hidden', status_code=400) 263
264 261
response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json') 265 262 response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json')
# This should fail because the user is not enrolled in section id 2 266 263 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 267 264 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
268 265
def test_unhide_flashcard(self): 269 266 def test_unhide_flashcard(self):
self.flashcard.hide_from(self.user) 270 267 self.flashcard.hide_from(self.user)
271 268
response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json') 272 269 response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 273 270 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
274 271
response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json') 275 272 response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json')
276 273
# This should fail because the user is not enrolled in section id 2 277 274 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 278 275 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
279 276
280 277
class SectionViewSetTest(APITestCase): 281 278 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 282 279 fixtures = ['testusers', 'testsections']
283 280
def setUp(self): 284 281 def setUp(self):
self.client.login(email='none@none.com', password='1234') 285 282 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 286 283 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), 287 284 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(),
author=self.user) 288 285 author=self.user)
flashcards/views.py View file @ 2958a18
import django 1 1 import django
2 2
from django.contrib import auth 3 3 from django.contrib import auth
from django.shortcuts import get_object_or_404 4 4 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection 5 5 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 6 6 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 7 7 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 8 8 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 9 9 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 10 10 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 11 11 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 12 12 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 13 13 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 14 14 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 15 15 from django.core.mail import send_mail
from django.contrib.auth import authenticate 16 16 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 17 17 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 18 18 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 19 19 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 20 20 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 21 21 from simple_email_confirmation import EmailAddress
22 22
23 23
class SectionViewSet(ReadOnlyModelViewSet): 24 24 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 25 25 queryset = Section.objects.all()
serializer_class = SectionSerializer 26 26 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 27 27 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 28 28 permission_classes = [IsAuthenticated]
29 29
@detail_route(methods=['GET']) 30 30 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 31 31 def flashcards(self, request, pk):
""" 32 32 """
Gets flashcards for a section, excluding hidden cards. 33 33 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 34 34 Returned in strictly chronological order (material date).
""" 35 35 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 36 36 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 37 37 return Response(FlashcardSerializer(flashcards, many=True).data)
38 38
@detail_route(methods=['post']) 39 39 @detail_route(methods=['post'])
def enroll(self, request, pk): 40 40 def enroll(self, request, pk):
""" 41 41 """
Add the current user to a specified section 42 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. 43 43 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 44 44 ---
omit_serializer: true 45 45 view_mocker: flashcards.api.mock_no_params
parameters: 46
- fake: None 47
parameters_strategy: 48
form: replace 49
""" 50 46 """
51 47
self.get_object().enroll(request.user) 52 48 self.get_object().enroll(request.user)
return Response(status=HTTP_204_NO_CONTENT) 53 49 return Response(status=HTTP_204_NO_CONTENT)
54 50
@detail_route(methods=['post']) 55 51 @detail_route(methods=['post'])
def drop(self, request, pk): 56 52 def drop(self, request, pk):
""" 57 53 """
Remove the current user from a specified section 58 54 Remove the current user from a specified section
If the user is not in the class, the request will fail. 59 55 If the user is not in the class, the request will fail.
--- 60 56 ---
omit_serializer: true 61 57 view_mocker: flashcards.api.mock_no_params
parameters: 62
- fake: None 63
parameters_strategy: 64
form: replace 65
""" 66 58 """
try: 67 59 try:
self.get_object().drop(request.user) 68 60 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: raise PermissionDenied(e) 69 61 except django.core.exceptions.PermissionDenied as e:
except django.core.exceptions.ValidationError as e: raise ValidationError(e) 70 62 raise PermissionDenied(e)
63 except django.core.exceptions.ValidationError as e:
64 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 71 65 return Response(status=HTTP_204_NO_CONTENT)
72 66
@list_route(methods=['GET']) 73 67 @list_route(methods=['GET'])
def search(self, request): 74 68 def search(self, request):
""" 75 69 """
Returns a list of sections which match a user's query 76 70 Returns a list of sections which match a user's query
71 ---
72 parameters:
73 - name: q
74 description: space-separated list of terms
75 required: true
76 type: form
""" 77 77 """
query = request.GET.get('q', None) 78 78 query = request.GET.get('q', None)
if not query: return Response('[]') 79 79 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 80 80 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 81 81 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 82 82 return Response(serializer.data)
83 83
@detail_route(methods=['GET']) 84 84 @detail_route(methods=['GET'])
def deck(self, request, pk): 85 85 def deck(self, request, pk):
""" 86 86 """
Gets the contents of a user's deck for a given section. 87 87 Gets the contents of a user's deck for a given section.
""" 88 88 """
qs = request.user.get_deck(self.get_object()) 89 89 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 90 90 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 91 91 return Response(serializer.data)
92 92
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 93 93 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 94 94 def ordered_deck(self, request, pk):
""" 95 95 """
Get a chronological order by material_date of flashcards for a section. 96 96 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 97 97 This excludes hidden card.
""" 98 98 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 99 99 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 100 100 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 101 101 return Response(serializer.data)
102 102
@detail_route(methods=['GET']) 103 103 @detail_route(methods=['GET'])
def feed(self, request, pk): 104 104 def feed(self, request, pk):
""" 105 105 """
Gets the contents of a user's feed for a section. 106 106 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 107 107 Exclude cards that are already in the user's deck
""" 108 108 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 109 109 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 110 110 return Response(serializer.data)
111 111
112 112
class UserSectionListView(ListAPIView): 113 113 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 114 114 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 115 115 permission_classes = [IsAuthenticated]
116 116
def get_queryset(self): 117 117 def get_queryset(self):
return self.request.user.sections.all() 118 118 return self.request.user.sections.all()
119 119
def paginate_queryset(self, queryset): return None 120 120 def paginate_queryset(self, queryset): return None
121 121
122 122
class UserDetail(GenericAPIView): 123 123 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 124 124 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 125 125 permission_classes = [IsAuthenticated]
126 126
def patch(self, request, format=None): 127 127 def patch(self, request, format=None):
""" 128 128 """
Updates the user's password, or verifies their email address 129 129 Updates the user's password, or verifies their email address
--- 130 130 ---
request_serializer: UserUpdateSerializer 131 131 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 132 132 response_serializer: UserSerializer
""" 133 133 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 134 134 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 135 135 data.is_valid(raise_exception=True)
data = data.validated_data 136 136 data = data.validated_data
137 137
if 'new_password' in data: 138 138 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 139 139 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 140 140 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 141 141 request.user.set_password(data['new_password'])
request.user.save() 142 142 request.user.save()
143 143
if 'confirmation_key' in data: 144 144 if 'confirmation_key' in data:
try: 145 145 try:
request.user.confirm_email(data['confirmation_key']) 146 146 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 147 147 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 148 148 raise ValidationError('confirmation_key is invalid')
149 149
return Response(UserSerializer(request.user).data) 150 150 return Response(UserSerializer(request.user).data)
151 151
def get(self, request, format=None): 152 152 def get(self, request, format=None):
""" 153 153 """
Return data about the user 154 154 Return data about the user
--- 155 155 ---
response_serializer: UserSerializer 156 156 response_serializer: UserSerializer
""" 157 157 """
serializer = UserSerializer(request.user, context={'request': request}) 158 158 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 159 159 return Response(serializer.data)
160 160
def delete(self, request): 161 161 def delete(self, request):
""" 162 162 """
Irrevocably delete the user and their data 163 163 Irrevocably delete the user and their data
164 164
Yes, really 165 165 Yes, really
""" 166 166 """
request.user.delete() 167 167 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 168 168 return Response(status=HTTP_204_NO_CONTENT)
169 169
170 170
@api_view(['POST']) 171 171 @api_view(['POST'])
def register(request, format=None): 172 172 def register(request, format=None):
""" 173 173 """
Register a new user 174 174 Register a new user
--- 175 175 ---
request_serializer: EmailPasswordSerializer 176 176 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 177 177 response_serializer: UserSerializer
""" 178 178 """
data = RegistrationSerializer(data=request.data) 179 179 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 180 180 data.is_valid(raise_exception=True)
181 181
User.objects.create_user(**data.validated_data) 182 182 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 183 183 user = authenticate(**data.validated_data)
auth.login(request, user) 184 184 auth.login(request, user)
185 185
body = ''' 186 186 body = '''
Visit the following link to confirm your email address: 187 187 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 188 188 https://flashy.cards/app/verifyemail/%s
189 189
If you did not register for Flashy, no action is required. 190 190 If you did not register for Flashy, no action is required.
''' 191 191 '''
192 192
assert send_mail("Flashy email verification", 193 193 assert send_mail("Flashy email verification",
body % user.confirmation_key, 194 194 body % user.confirmation_key,
"noreply@flashy.cards", 195 195 "noreply@flashy.cards",
[user.email]) 196 196 [user.email])
197 197
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 198 198 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
199 199
200 200
@api_view(['POST']) 201 201 @api_view(['POST'])
def login(request): 202 202 def login(request):
""" 203 203 """
Authenticates user and returns user data if valid. 204 204 Authenticates user and returns user data if valid.
--- 205 205 ---
request_serializer: EmailPasswordSerializer 206 206 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 207 207 response_serializer: UserSerializer
""" 208 208 """
209 209
data = EmailPasswordSerializer(data=request.data) 210 210 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 211 211 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 212 212 user = authenticate(**data.validated_data)
213 213
if user is None: 214 214 if user is None:
raise AuthenticationFailed('Invalid email or password') 215 215 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 216 216 if not user.is_active:
raise NotAuthenticated('Account is disabled') 217 217 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 218 218 auth.login(request, user)
return Response(UserSerializer(request.user).data) 219 219 return Response(UserSerializer(request.user).data)
220 220
221 221
@api_view(['POST']) 222 222 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 223 223 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 224 224 def logout(request, format=None):
""" 225 225 """
Logs the authenticated user out. 226 226 Logs the authenticated user out.
""" 227 227 """
auth.logout(request) 228 228 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 229 229 return Response(status=HTTP_204_NO_CONTENT)
230 230
231 231
@api_view(['POST']) 232 232 @api_view(['POST'])
def request_password_reset(request, format=None): 233 233 def request_password_reset(request, format=None):
""" 234 234 """
Send a password reset token/link to the provided email. 235 235 Send a password reset token/link to the provided email.
--- 236 236 ---
request_serializer: PasswordResetRequestSerializer 237 237 request_serializer: PasswordResetRequestSerializer
""" 238 238 """
data = PasswordResetRequestSerializer(data=request.data) 239 239 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 240 240 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 241 241 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 242 242 token = default_token_generator.make_token(user)
243 243
body = ''' 244 244 body = '''
Visit the following link to reset your password: 245 245 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 246 246 https://flashy.cards/app/resetpassword/%d/%s
247 247
If you did not request a password reset, no action is required. 248 248 If you did not request a password reset, no action is required.
''' 249 249 '''
250 250
send_mail("Flashy password reset", 251 251 send_mail("Flashy password reset",
body % (user.pk, token), 252 252 body % (user.pk, token),
"noreply@flashy.cards", 253 253 "noreply@flashy.cards",
[user.email]) 254 254 [user.email])
255 255
return Response(status=HTTP_204_NO_CONTENT) 256 256 return Response(status=HTTP_204_NO_CONTENT)
257 257
258 258
@api_view(['POST']) 259 259 @api_view(['POST'])
def reset_password(request, format=None): 260 260 def reset_password(request, format=None):
""" 261 261 """
Updates user's password to new password if token is valid. 262 262 Updates user's password to new password if token is valid.
--- 263 263 ---
request_serializer: PasswordResetSerializer 264 264 request_serializer: PasswordResetSerializer
""" 265 265 """
data = PasswordResetSerializer(data=request.data) 266 266 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 267 267 data.is_valid(raise_exception=True)
268 268
user = User.objects.get(id=data['uid'].value) 269 269 user = User.objects.get(id=data['uid'].value)
# Check token validity. 270 270 # Check token validity.
271 271
if default_token_generator.check_token(user, data['token'].value): 272 272 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 273 273 user.set_password(data['new_password'].value)
user.save() 274 274 user.save()
else: 275 275 else:
raise ValidationError('Could not verify reset token') 276 276 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 277 277 return Response(status=HTTP_204_NO_CONTENT)
278 278
279 279
class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): 280 280 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 281 281 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 282 282 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 283 283 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
284 284
# Override create in CreateModelMixin 285 285 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 286 286 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 287 287 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 288 288 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 289 289 data = serializer.validated_data
290 if not request.user.is_in_section(data['section']):
291 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 290 292 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 291 293 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 292 294 self.perform_create(flashcard)
headers = self.get_success_headers(data) 293 295 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard) 294 296 response_data = FlashcardSerializer(flashcard)
return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) 295 297 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
296 298
@detail_route(methods=['post']) 297
def hide(self, request, pk): 298
""" 299
Hide a flashcard 300
--- 301
omit_serializer: true 302
parameters: 303
- fake: None 304
parameters_strategy: 305
form: replace 306
""" 307
try: 308
self.get_object().hide_from(request.user) 309
except django.core.exceptions.ValidationError: 310
raise ValidationError("The card has already been hidden.") 311
return Response(status=HTTP_204_NO_CONTENT) 312
313 299
@detail_route(methods=['post']) 314 300 @detail_route(methods=['post'])
def unhide(self, request, pk): 315 301 def unhide(self, request, pk):
""" 316 302 """
Report the given card 317 303 Unhide the given card
--- 318 304 ---
omit_serializer: true 319 305 view_mocker: flashcards.api.mock_no_params
parameters: 320
- fake: None 321
parameters_strategy: 322
form: replace 323
""" 324 306 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 325 307 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 326 308 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 327 309 return Response(status=HTTP_204_NO_CONTENT)
328 310
@detail_route(methods=['post']) 329 311 @detail_route(methods=['post'])
def report(self, request, pk): 330 312 def report(self, request, pk):
""" 331 313 """
Report the given card 332 314 Hide the given card
--- 333 315 ---
omit_serializer: true 334 316 view_mocker: flashcards.api.mock_no_params
parameters: 335
- fake: None 336
parameters_strategy: 337
form: replace 338
""" 339 317 """
obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) 340 318 obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object())
obj.reason = request.data['reason'] 341 319 obj.reason = request.data.get('reason', None)
if created: 342 320 obj.save()
obj.save() 343
return Response(status=HTTP_204_NO_CONTENT) 344 321 return Response(status=HTTP_204_NO_CONTENT)
345 322
323 hide = report
324
@detail_route(methods=['POST']) 346 325 @detail_route(methods=['POST'])
def pull(self, request, pk): 347 326 def pull(self, request, pk):
""" 348 327 """
Pull a card from the live feed into the user's deck. 349 328 Pull a card from the live feed into the user's deck.
:param request: The request object 350 329 ---
:param pk: The primary key 351 330 view_mocker: flashcards.api.mock_no_params
:return: A 204 response upon success. 352
""" 353 331 """
user = request.user 354 332 user = request.user
flashcard = self.get_object() 355 333 flashcard = self.get_object()