Commit 5769450a64e1fb8413aca028936ff578ebe18fd4

Authored by Laura Hawkins
Exists in master

trying to pull

Showing 15 changed files Inline Diff

Flashy requires Python 2. Srsly 1 1 Flashy requires Python 2. Srsly
2 2
All of these commands should be run from this directory (the one containing README.md) 3 3 All of these commands should be run from this directory (the one containing README.md)
4 4
Virtualenv for Windows creates a dir inexplicably named scripts rather than bin. So substitute venv/bin for venv/scripts if you are on Windows. 5 5 Virtualenv for Windows creates a dir inexplicably named scripts rather than bin. So substitute venv/bin for venv/scripts if you are on Windows.
6 6
Install virtualenv before continuing. This is most easily accomplished with: 7 7 Install virtualenv before continuing. This is most easily accomplished with:
8 8
pip install virtualenv 9 9 pip install virtualenv
10 10
Set up the environment by running: 11 11 Set up the environment by running:
12 12
scripts/setup.sh 13 13 scripts/setup.sh
14 14
If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv: 15 15 If you get errors about a module not being found, make sure you are working in the virtualenv. To enter the venv:
16 16
. venv/bin/activate 17 17 . venv/bin/activate
18 18
If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run: 19 19 If you still get errors about a module not being found, make sure your virtualenv is up to date. Re-run:
20 20
scripts/setup.sh 21 21 scripts/setup.sh
22 22
flashcards/api.py View file @ 5769450
1 from flashcards.models import Flashcard
from rest_framework.pagination import PageNumberPagination 1 2 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 2 3 from rest_framework.permissions import BasePermission
3 4
4 5
6 mock_no_params = lambda x:None
7
class StandardResultsSetPagination(PageNumberPagination): 5 8 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 6 9 page_size = 40
page_size_query_param = 'page_size' 7 10 page_size_query_param = 'page_size'
max_page_size = 1000 8 11 max_page_size = 1000
9 12
10 13
class UserDetailPermissions(BasePermission): 11 14 class UserDetailPermissions(BasePermission):
""" 12 15 """
Permissions for the user detail view. Anonymous users may only POST. 13 16 Permissions for the user detail view. Anonymous users may only POST.
""" 14 17 """
18
def has_object_permission(self, request, view, obj): 15 19 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 16 20 if request.method == 'POST':
return True 17 21 return True
return request.user.is_authenticated() 18 22 return request.user.is_authenticated()
23
24
flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py View file @ 5769450
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/0013_auto_20150516_2356.py View file @ 5769450
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', '0012_auto_20150516_0313'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='flashcard',
16 name='hide_reason',
17 field=models.CharField(default=None, max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True),
18 ),
19 migrations.AlterField(
flashcards/migrations/0013_auto_20150517_0402.py View file @ 5769450
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', '0012_auto_20150516_0313'),
11 ]
12
13 operations = [
14 migrations.AlterField(
15 model_name='flashcard',
16 name='hide_reason',
17 field=models.CharField(default=None, help_text=b'Reason for hiding this card', max_length=255, blank=True),
18 ),
19 migrations.AlterField(
flashcards/migrations/0014_merge.py View file @ 5769450
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', '0013_auto_20150517_0402'),
flashcards/migrations/0015_auto_20150518_0017.py View file @ 5769450
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 @ 5769450
from datetime import datetime 1
2
from django.contrib.auth.models import AbstractUser, UserManager 3 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied 4 2 from django.contrib.auth.tokens import default_token_generator
3 from django.core.cache import cache
4 from django.core.exceptions import ValidationError
5 from django.core.exceptions import PermissionDenied, SuspiciousOperation
6 from django.core.mail import send_mail
7 from django.db import IntegrityError
from django.db.models import * 5 8 from django.db.models import *
from django.utils.timezone import now 6 9 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 7 10 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 8 11 from fields import MaskField
9 12
10 13
# Hack to fix AbstractUser before subclassing it 11 14 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 12 15 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 13 16 AbstractUser._meta.get_field('username')._unique = False
14 17
15 18
class EmailOnlyUserManager(UserManager): 16 19 class EmailOnlyUserManager(UserManager):
""" 17 20 """
A tiny extension of Django's UserManager which correctly creates users 18 21 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 19 22 without usernames (using emails instead).
""" 20 23 """
21 24
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 22 25 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 23 26 """
Creates and saves a User with the given email and password. 24 27 Creates and saves a User with the given email and password.
""" 25 28 """
email = self.normalize_email(email) 26 29 email = self.normalize_email(email)
user = self.model(email=email, 27 30 user = self.model(email=email,
is_staff=is_staff, is_active=True, 28 31 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 29 32 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 30 33 date_joined=now(), **extra_fields)
user.set_password(password) 31 34 user.set_password(password)
user.save(using=self._db) 32 35 user.save(using=self._db)
return user 33 36 return user
34 37
def create_user(self, email, password=None, **extra_fields): 35 38 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 36 39 user = self._create_user(email, password, False, False, **extra_fields)
40 body = '''
41 Visit the following link to confirm your email address:
42 https://flashy.cards/app/verifyemail/%s
37 43
44 If you did not register for Flashy, no action is required.
45 '''
46
47 assert send_mail("Flashy email verification",
48 body % user.confirmation_key,
49 "noreply@flashy.cards",
50 [user.email])
51 return user
52
def create_superuser(self, email, password, **extra_fields): 38 53 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 39 54 return self._create_user(email, password, True, True, **extra_fields)
40 55
41 56
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 42 57 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 43 58 """
An extension of Django's default user model. 44 59 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 45 60 We use email as the username field, and include enrolled sections here
""" 46 61 """
objects = EmailOnlyUserManager() 47 62 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 48 63 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 49 64 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 50 65 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
51 66
def is_in_section(self, section): 52 67 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 53 68 return self.sections.filter(pk=section.pk).exists()
54 69
def pull(self, flashcard): 55 70 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 56 71 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 57 72 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 58 73 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 59 74 user_card.pulled = now()
user_card.save() 60 75 user_card.save()
61 76
def unpull(self, flashcard): 62 77 def unpull(self, flashcard):
user = self 63 78 if not self.is_in_section(flashcard.section):
result = user.userflashcard_set.filter(flashcard=self) 64 79 raise ValueError("User not in the section this flashcard belongs to")
if not result.exists(): raise ValidationError ('You cannot remove this flashcard.') 65
66 80
81 try:
82 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
83 except UserFlashcard.DoesNotExist:
84 raise ValueError('Cannot unpull card that is not pulled.')
85
86 user_card.delete()
87
def get_deck(self, section): 67 88 def get_deck(self, section):
if not self.is_in_section(section): 68 89 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 69 90 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 70 91 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
71 92
93 def request_password_reset(self):
94 token = default_token_generator.make_token(self)
72 95
96 body = '''
97 Visit the following link to reset your password:
98 https://flashy.cards/app/resetpassword/%d/%s
99
100 If you did not request a password reset, no action is required.
101 '''
102
103 send_mail("Flashy password reset",
104 body % (self.pk, token),
105 "noreply@flashy.cards",
106 [self.email])
107
108
class UserFlashcard(Model): 73 109 class UserFlashcard(Model):
""" 74 110 """
Represents the relationship between a user and a flashcard by: 75 111 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 76 112 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 77 113 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 78 114 3. A user has a flashcard hidden from them
""" 79 115 """
user = ForeignKey('User') 80 116 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 81 117 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 82 118 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") 83 119 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 84 120 flashcard = ForeignKey('Flashcard')
85 121
class Meta: 86 122 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 87 123 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 88 124 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 89 125 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 90 126 # By default, order by most recently pulled
ordering = ['-pulled'] 91 127 ordering = ['-pulled']
92 128
93 129
class FlashcardHide(Model): 94 130 class FlashcardHide(Model):
""" 95 131 """
Represents the property of a flashcard being hidden by a user. 96 132 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 97 133 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 98 134 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. 99 135 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 100 136 """
user = ForeignKey('User') 101 137 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 102 138 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 103 139 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 104 140 hidden = DateTimeField(auto_now_add=True)
105 141
class Meta: 106 142 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 107 143 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 108 144 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 109 145 index_together = ["user", "flashcard"]
110 146
111 147
class Flashcard(Model): 112 148 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 113 149 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') 114 150 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") 115 151 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") 116 152 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 117 153 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 118 154 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 119 155 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 120 156 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 121 157 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
158 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 122 159 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
123 160
class Meta: 124 161 class Meta:
# By default, order by most recently pushed 125 162 # By default, order by most recently pushed
ordering = ['-pushed'] 126 163 ordering = ['-pushed']
127 164
def is_hidden_from(self, user): 128 165 def is_hidden_from(self, user):
""" 129 166 """
A card can be hidden globally, but if a user has the card in their deck, 130 167 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 131 168 this visibility overrides a global hide.
:param user: 132 169 :param user:
:return: Whether the card is hidden from the user. 133 170 :return: Whether the card is hidden from the user.
""" 134 171 """
result = user.userflashcard_set.filter(flashcard=self) 135 172 if self.userflashcard_set.filter(user=user).exists(): return False
if not result.exists(): return self.is_hidden 136 173 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return result[0].is_hidden() 137 174 return False
138 175
def edit(self, user, new_flashcard): 139 176 def hide_from(self, user, reason=None):
177 if self.is_in_deck(user): user.unpull(self)
178 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
179 if not created:
180 raise ValidationError("The card has already been hidden.")
181 obj.save()
182
183 def is_in_deck(self, user):
184 return self.userflashcard_set.filter(user=user).exists()
185
186 def add_to_deck(self, user):
187 if not user.is_in_section(self.section):
188 raise PermissionDenied("You don't have the permission to add this card")
189 try:
190 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
191 except IntegrityError:
192 raise SuspiciousOperation("The flashcard is already in the user's deck")
193 user_flashcard.save()
194 return user_flashcard
195
196 def edit(self, user, new_data):
""" 140 197 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 141 198 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. 142 199 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 143 200 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 144 201 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 145 202 """
if not user.is_in_section(self.section): 146
raise PermissionDenied("You don't have the permission to edit this card") 147
148 203
# content_changed is True iff either material_date or text were changed 149 204 # content_changed is True iff either material_date or text were changed
content_changed = False 150 205 content_changed = False
# create_new is True iff the user editing this card is the author of this card 151 206 # 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 152 207 # and there are no other users with this card in their decks
create_new = user != self.author or \ 153 208 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 154 209 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
155 210 if 'material_date' in new_data and self.material_date != new_data['material_date']:
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 156 211 content_changed = True
content_changed |= True 157 212 self.material_date = new_data['material_date']
self.material_date = new_flashcard['material_date'] 158 213 if 'text' in new_data and self.text != new_data['text']:
if 'text' in new_flashcard and self.text != new_flashcard['text']: 159 214 content_changed = True
content_changed |= True 160 215 self.text = new_data['text']
self.text = new_flashcard['text'] 161
if create_new and content_changed: 162 216 if create_new and content_changed:
217 if self.is_in_deck(user): user.unpull(self)
218 self.previous_id = self.pk
self.pk = None 163 219 self.pk = None
if 'mask' in new_flashcard: 164 220 self.mask = new_data.get('mask', self.mask)
self.mask = new_flashcard['mask'] 165
self.save() 166 221 self.save()
222 self.add_to_deck(user)
223 else:
224 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
225 user_card.mask = new_data.get('mask', user_card.mask)
226 user_card.save()
227 return self
167 228
229 def report(self, user, reason=None):
230 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
231 obj.reason = reason
232 obj.save()
233
@classmethod 168 234 @classmethod
def cards_visible_to(cls, user): 169 235 def cards_visible_to(cls, user):
""" 170 236 """
:param user: 171 237 :param user:
:return: A queryset with all cards that should be visible to a user. 172 238 :return: A queryset with all cards that should be visible to a user.
""" 173 239 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 174 240 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
175 241
176 242
class UserFlashcardQuiz(Model): 177 243 class UserFlashcardQuiz(Model):
""" 178 244 """
An event of a user being quizzed on a flashcard. 179 245 An event of a user being quizzed on a flashcard.
""" 180 246 """
user_flashcard = ForeignKey(UserFlashcard) 181 247 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 182 248 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 183 249 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") 184 250 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") 185 251 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
186 252
def status(self): 187 253 def status(self):
""" 188 254 """
There are three stages of a quiz object: 189 255 There are three stages of a quiz object:
1. the user has been shown the card 190 256 1. the user has been shown the card
2. the user has answered the card 191 257 2. the user has answered the card
3. the user has self-evaluated their response's correctness 192 258 3. the user has self-evaluated their response's correctness
193 259
:return: string (evaluated, answered, viewed) 194 260 :return: string (evaluated, answered, viewed)
""" 195 261 """
if self.correct is not None: return "evaluated" 196 262 if self.correct is not None: return "evaluated"
if self.response: return "answered" 197 263 if self.response: return "answered"
return "viewed" 198 264 return "viewed"
199 265
200 266
class Section(Model): 201 267 class Section(Model):
""" 202 268 """
A UCSD course taught by an instructor during a quarter. 203 269 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 204 270 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 205 271 We index gratuitously to support autofill and because this is primarily read-only
""" 206 272 """
department = CharField(db_index=True, max_length=50) 207 273 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 208 274 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 209 275 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 210 276 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 211 277 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 212 278 quarter = CharField(db_index=True, max_length=4)
213 279
@classmethod 214 280 @classmethod
def search(cls, terms): 215 281 def search(cls, terms):
""" 216 282 """
Search all fields of all sections for a particular set of terms 217 283 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 218 284 A matching section must match at least one field on each term
:param terms:iterable 219 285 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 220 286 :return: Matching QuerySet ordered by department and course number
""" 221 287 """
final_q = Q() 222 288 final_q = Q()
for term in terms: 223 289 for term in terms:
q = Q(department__icontains=term) 224 290 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 225 291 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 226 292 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 227 293 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 228 294 q |= Q(instructor__icontains=term)
final_q &= q 229 295 final_q &= q
qs = cls.objects.filter(final_q) 230 296 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 231 297 # 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 232 298 # 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)"}) 233 299 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 234 300 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 235 301 return qs
236 302
@property 237 303 @property
def is_whitelisted(self): 238 304 def is_whitelisted(self):
""" 239 305 """
:return: whether a whitelist exists for this section 240 306 :return: whether a whitelist exists for this section
""" 241 307 """
return self.whitelist.exists() 242 308 return self.whitelist.exists()
243 309
def is_user_on_whitelist(self, user): 244 310 def is_user_on_whitelist(self, user):
""" 245 311 """
:return: whether the user is on the waitlist for this section 246 312 :return: whether the user is on the waitlist for this section
""" 247 313 """
return self.whitelist.filter(email=user.email).exists() 248 314 return self.whitelist.filter(email=user.email).exists()
249 315
316
317 def enroll(self, user):
318 if user.sections.filter(pk=self.pk).exists():
319 raise ValidationError('User is already enrolled in this section')
320 if self.is_whitelisted and not self.is_user_on_whitelist(user):
321 raise PermissionDenied("User must be on the whitelist to add this section.")
322 self.user_set.add(user)
323
324 def drop(self, user):
325 if not user.sections.filter(pk=self.pk).exists():
326 raise ValidationError("User is not enrolled in the section.")
327 self.user_set.remove(user)
328
class Meta: 250 329 class Meta:
ordering = ['-course_title'] 251 330 ordering = ['-course_title']
252 331
@property 253 332 @property
def lecture_times(self): 254 333 def lecture_times(self):
lecture_periods = self.lectureperiod_set.all() 255 334 data = cache.get("section_%d_lecture_times" % self.pk)
if not lecture_periods.exists(): return '' 256 335 if not data:
return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time 257 336 lecture_periods = self.lectureperiod_set.all()
337 if lecture_periods.exists():
338 data = ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[
339 0].short_start_time
340 else:
341 data = ''
342 cache.set("section_%d_lecture_times" % self.pk, data, 24*60*60)
343 return data
258 344
345
@property 259 346 @property
def long_name(self): 260 347 def long_name(self):
return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor) 261 348 return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor)
262 349
@property 263 350 @property
def short_name(self): 264 351 def short_name(self):
return '%s %s' % (self.department_abbreviation, self.course_num) 265 352 return '%s %s' % (self.department_abbreviation, self.course_num)
266 353
def get_feed_for_user(self, user): 267 354 def get_feed_for_user(self, user):
qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed') 268 355 qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed')
return qs 269 356 return qs
270 357
358 def get_cards_for_user(self, user):
359 return Flashcard.cards_visible_to(user).filter(section=self)
360
def __unicode__(self): 271 361 def __unicode__(self):
return '%s %s: %s (%s %s)' % ( 272 362 return '%s %s: %s (%s %s)' % (
self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) 273 363 self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter)
274 364
275 365
276 366
277 367
flashcards/serializers.py View file @ 5769450
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
from rest_framework.serializers import ModelSerializer, Serializer 9 10 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 10 11 from rest_framework.validators import UniqueValidator
from json import dumps, loads 11
12 12
13 13
class EmailSerializer(Serializer): 14 14 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 15 email = EmailField(required=True)
16 16
17 17
class EmailPasswordSerializer(EmailSerializer): 18 18 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 19 password = CharField(required=True)
20 20
21 21
class RegistrationSerializer(EmailPasswordSerializer): 22 22 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 23 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 24
25 25
class PasswordResetRequestSerializer(EmailSerializer): 26 26 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 27 def validate_email(self, value):
try: 28 28 try:
User.objects.get(email=value) 29 29 User.objects.get(email=value)
return value 30 30 return value
except User.DoesNotExist: 31 31 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 32 raise serializers.ValidationError('No user exists with that email')
33 33
34 34
class PasswordResetSerializer(Serializer): 35 35 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 36 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 37 uid = IntegerField(required=True)
token = CharField(required=True) 38 38 token = CharField(required=True)
39 39
def validate_uid(self, value): 40 40 def validate_uid(self, value):
try: 41 41 try:
User.objects.get(id=value) 42 42 User.objects.get(id=value)
return value 43 43 return value
except User.DoesNotExist: 44 44 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 45 raise serializers.ValidationError('Could not verify reset token')
46 46
47 47
class UserUpdateSerializer(Serializer): 48 48 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 49 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 50 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 51 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 52 # reset_token = CharField(required=False)
53 53
def validate(self, data): 54 54 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 55 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 56 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 57 return data
58 58
59 59
class Password(Serializer): 60 60 class Password(Serializer):
email = EmailField(required=True) 61 61 email = EmailField(required=True)
password = CharField(required=True) 62 62 password = CharField(required=True)
63 63
64 64
class LecturePeriodSerializer(ModelSerializer): 65 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 66 class Meta:
model = LecturePeriod 67 67 model = LecturePeriod
exclude = 'id', 'section' 68 68 exclude = 'id', 'section'
69 69
70 70
class SectionSerializer(ModelSerializer): 71 71 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 72
lecture_times = CharField() 73 72 lecture_times = CharField()
short_name = CharField() 74 73 short_name = CharField()
long_name = CharField() 75 74 long_name = CharField()
76 75
class Meta: 77 76 class Meta:
model = Section 78 77 model = Section
79 78
79 class DeepSectionSerializer(SectionSerializer):
80 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
80 81
82
83
class UserSerializer(ModelSerializer): 81 84 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 82 85 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 83 86 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 84 87 is_confirmed = BooleanField()
85 88
class Meta: 86 89 class Meta:
model = User 87 90 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 88 91 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
89 92
90 93
class MaskFieldSerializer(serializers.Field): 91 94 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 92 95 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 93 96 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 94 97 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 95 98 'overlap': 'Ensure this field does not have overlapping intervals.'
} 96 99 }
97 100
def to_representation(self, value): 98 101 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 99 102 return dumps(list(self._make_mask(value)))
100 103
def to_internal_value(self, value): 101 104 def to_internal_value(self, value):
return self._make_mask(loads(value)) 102 105 return self._make_mask(loads(value))
103 106
def _make_mask(self, data): 104 107 def _make_mask(self, data):
try: 105 108 try:
mask = FlashcardMask(data) 106 109 mask = FlashcardMask(data)
except ValueError: 107 110 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 108 111 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 109 112 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 110 113 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 111 114 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 112 115 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 113 116 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 114 117 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 115 118 return mask
116 119
117 120
class FlashcardSerializer(ModelSerializer): 118 121 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 119 122 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 120 123 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 121 124 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer() 122 125 mask = MaskFieldSerializer(allow_null=True)
123 126
def validate_material_date(self, value): 124 127 def validate_material_date(self, value):
utc = pytz.UTC 125 128 utc = pytz.UTC
# TODO: make this dynamic 126 129 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 127 130 quarter_start = utc.localize(datetime(2015, 3, 15))
quarter_end = utc.localize(datetime(2015, 6, 15)) 128 131 quarter_end = utc.localize(datetime(2015, 6, 15))
129 132
if quarter_start <= value <= quarter_end: 130 133 if quarter_start <= value <= quarter_end:
return value 131 134 return value
else: 132 135 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 133 136 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
134 137
def validate_previous(self, value): 135
if value is None: 136
return value 137
if Flashcard.objects.filter(pk=value).count() > 0: 138
return value 139
raise serializers.ValidationError("Invalid previous Flashcard object") 140
141
def validate_pushed(self, value): 142 138 def validate_pushed(self, value):
if value > datetime.now(): 143 139 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 144 140 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 145 141 return value
146 142
def validate_text(self, value): 147
if len(value) > 255: 148
raise serializers.ValidationError("Flashcard text limit exceeded") 149
flashcards/tests/test_api.py View file @ 5769450
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
6 import datetime
from django.utils.timezone import now 6 7 from django.utils.timezone import now
8 from flashcards.validators import FlashcardMask
9 from flashcards.serializers import FlashcardSerializer
7 10
8 11
class LoginTests(APITestCase): 9 12 class LoginTests(APITestCase):
fixtures = ['testusers'] 10 13 fixtures = ['testusers']
11 14
def test_login(self): 12 15 def test_login(self):
url = '/api/login' 13 16 url = '/api/login/'
data = {'email': 'none@none.com', 'password': '1234'} 14 17 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 15 18 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 16 19 self.assertEqual(response.status_code, HTTP_200_OK)
17 20
data = {'email': 'none@none.com', 'password': '4321'} 18 21 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 19 22 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 20 23 self.assertContains(response, 'Invalid email or password', status_code=403)
21 24
data = {'email': 'bad@none.com', 'password': '1234'} 22 25 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 23 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 24 27 self.assertContains(response, 'Invalid email or password', status_code=403)
25 28
data = {'password': '4321'} 26 29 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 27 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 28 31 self.assertContains(response, 'email', status_code=400)
29 32
data = {'email': 'none@none.com'} 30 33 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 31 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 32 35 self.assertContains(response, 'password', status_code=400)
33 36
user = User.objects.get(email="none@none.com") 34 37 user = User.objects.get(email="none@none.com")
user.is_active = False 35 38 user.is_active = False
user.save() 36 39 user.save()
37 40
data = {'email': 'none@none.com', 'password': '1234'} 38 41 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 39 42 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 40 43 self.assertContains(response, 'Account is disabled', status_code=403)
41 44
def test_logout(self): 42 45 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 43 46 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout') 44 47 response = self.client.post('/api/logout/')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 45 48 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
46 49
# since we're not logged in, we should get a 403 response 47 50 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me', format='json') 48 51 response = self.client.get('/api/me/', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 49 52 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
50 53
51 54
class PasswordResetTest(APITestCase): 52 55 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 53 56 fixtures = ['testusers']
54 57
def test_reset_password(self): 55 58 def test_reset_password(self):
# submit the request to reset the password 56 59 # submit the request to reset the password
url = '/api/request_password_reset' 57 60 url = '/api/request_password_reset/'
post_data = {'email': 'none@none.com'} 58 61 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 59 62 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 60 63 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 61 64 self.assertIn('reset your password', mail.outbox[0].body)
62 65
# capture the reset token from the email 63 66 # capture the reset token from the email
capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', 64 67 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 65 68 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 66 69 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 67 70 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 68 71 reset_token = capture.group(2)
69 72
# try to reset the password with the wrong reset token 70 73 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 71 74 patch_data['token'] = 'wrong_token'
url = '/api/reset_password' 72 75 url = '/api/reset_password/'
response = self.client.post(url, patch_data, format='json') 73 76 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 74 77 self.assertContains(response, 'Could not verify reset token', status_code=400)
75 78
# try to reset the password with the correct token 76 79 # try to reset the password with the correct token
patch_data['token'] = reset_token 77 80 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 78 81 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 79 82 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 80 83 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 81 84 assert user.check_password(patch_data['new_password'])
82 85
83 86
class RegistrationTest(APITestCase): 84 87 class RegistrationTest(APITestCase):
def test_create_account(self): 85 88 def test_create_account(self):
url = '/api/register' 86 89 url = '/api/register/'
87 90
# missing password 88 91 # missing password
data = {'email': 'none@none.com'} 89 92 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 90 93 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 91 94 self.assertContains(response, 'password', status_code=400)
92 95
# missing email 93 96 # missing email
data = {'password': '1234'} 94 97 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 95 98 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 96 99 self.assertContains(response, 'email', status_code=400)
97 100
# create a user 98 101 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 99 102 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 100 103 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 101 104 self.assertEqual(response.status_code, HTTP_201_CREATED)
102 105
# user should not be confirmed 103 106 # user should not be confirmed
user = User.objects.get(email="none@none.com") 104 107 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 105 108 self.assertFalse(user.is_confirmed)
106 109
# check that the confirmation key was sent 107 110 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 108 111 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 109 112 self.assertIn(user.confirmation_key, mail.outbox[0].body)
110 113
# log the user out 111 114 # log the user out
self.client.logout() 112 115 self.client.logout()
113 116
# log the user in with their registered credentials 114 117 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 115 118 self.client.login(email='none@none.com', password='1234')
116 119
# try activating with an invalid key 117 120 # try activating with an invalid key
118 121
url = '/api/me' 119 122 url = '/api/me/'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 120 123 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 121 124 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
122 125
# try activating with the valid key 123 126 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 124 127 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 125 128 self.assertTrue(response.data['is_confirmed'])
126 129
127 130
class ProfileViewTest(APITestCase): 128 131 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 129 132 fixtures = ['testusers']
130 133
def test_get_me(self): 131 134 def test_get_me(self):
url = '/api/me' 132 135 url = '/api/me/'
response = self.client.get(url, format='json') 133 136 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 134 137 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 135 138 self.assertEqual(response.status_code, 403)
136 139
self.client.login(email='none@none.com', password='1234') 137 140 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 138 141 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 139 142 self.assertEqual(response.status_code, HTTP_200_OK)
140 143
141 144
145 class UserSectionsTest(APITestCase):
146 fixtures = ['testusers', 'testsections']
147
148 def setUp(self):
149 self.user = User.objects.get(pk=1)
150 self.client.login(email='none@none.com', password='1234')
151 self.section = Section.objects.get(pk=1)
152 self.section.enroll(self.user)
153
154 def test_get_user_sections(self):
155 response = self.client.get('/api/me/sections/', format='json')
156 self.assertEqual(response.status_code, 200)
157 self.assertContains(response, 'Goldstein')
158
159
class PasswordChangeTest(APITestCase): 142 160 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 143 161 fixtures = ['testusers']
144 162
def test_change_password(self): 145 163 def test_change_password(self):
url = '/api/me' 146 164 url = '/api/me/'
user = User.objects.get(email='none@none.com') 147 165 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 148 166 self.assertTrue(user.check_password('1234'))
149 167
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 150 168 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 151 169 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
152 170
self.client.login(email='none@none.com', password='1234') 153 171 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 154 172 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 155 173 self.assertContains(response, 'old_password is required', status_code=400)
156 174
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 157 175 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 158 176 self.assertContains(response, 'old_password is incorrect', status_code=400)
159 177
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 160 178 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 161 179 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 162 180 user = User.objects.get(email='none@none.com')
163 181
self.assertFalse(user.check_password('1234')) 164 182 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 165 183 self.assertTrue(user.check_password('4321'))
166 184
167 185
class DeleteUserTest(APITestCase): 168 186 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 169 187 fixtures = ['testusers']
170 188
def test_delete_user(self): 171 189 def test_delete_user(self):
url = '/api/me' 172 190 url = '/api/me/'
user = User.objects.get(email='none@none.com') 173 191 user = User.objects.get(email='none@none.com')
174 192
self.client.login(email='none@none.com', password='1234') 175 193 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 176 194 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 177 195 self.assertFalse(User.objects.filter(email='none@none.com').exists())
178 196
179 197
class FlashcardDetailTest(APITestCase): 180 198 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 181 199 fixtures = ['testusers', 'testsections']
182 200
def setUp(self): 183 201 def setUp(self):
section = Section.objects.get(pk=1) 184 202 self.section = Section.objects.get(pk=1)
user = User.objects.get(email='none@none.com') 185 203 self.user = User.objects.get(email='none@none.com')
186 204 self.section.enroll(self.user)
self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) 187 205 self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2),
206 author=self.user)
207 self.inaccessible_flashcard.save()
208 self.flashcard = Flashcard(text="jason", section=self.section, author=self.user)
self.flashcard.save() 188 209 self.flashcard.save()
210 #self.flashcard.add_to_deck(self.user)
211 self.client.login(email='none@none.com', password='1234')
189 212
213 def test_edit_flashcard(self):
214 user = self.user
215 flashcard = self.flashcard
216 url = "/api/flashcards/{}/".format(flashcard.pk)
217 data = {'text': 'new wow for the flashcard',
218 'mask': '[[0,4]]'}
219 self.assertNotEqual(flashcard.text, data['text'])
220 response = self.client.patch(url, data, format='json')
221 self.assertEqual(response.status_code, HTTP_200_OK)
222 self.assertEqual(response.data['text'], data['text'])
223 data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2),
224 'mask': '[[1, 3]]'}
225 user2 = User.objects.create(email='wow@wow.wow', password='wow')
226 user2.sections.add(self.section)
227 user2.save()
228 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
229 response = self.client.patch(url, data, format='json')
230 serializer = FlashcardSerializer(data=response.data)
231 serializer.is_valid(raise_exception=True)
232 self.assertEqual(response.status_code, HTTP_200_OK)
233 # self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date']))
234 self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]]))
235 data = {'mask': '[[3,6]]'}
236 response = self.client.patch(url, data, format='json')
237 user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard)
238 self.assertEqual(response.status_code, HTTP_200_OK)
239 self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]]))
240
241 def test_create_flashcard(self):
242 data = {'text': 'this is a flashcard',
243 'material_date': now(),
244 'mask': '[]',
245 'section': '1',
246 'previous': None}
247 response = self.client.post("/api/flashcards/", data, format="json")
248 self.assertEqual(response.status_code, HTTP_201_CREATED)
249 self.assertEqual(response.data['text'], data['text'])
250 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
251
def test_get_flashcard(self): 190 252 def test_get_flashcard(self):
self.client.login(email='none@none.com', password='1234') 191
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 192 253 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 193 254 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 194 255 self.assertEqual(response.data["text"], "jason")
195 256
257 def test_hide_flashcard(self):
258 response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json')
259 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
260 self.assertTrue(self.flashcard.is_hidden_from(self.user))
196 261
262 response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json')
263 # This should fail because the user is not enrolled in section id 2
264 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
265
266 def test_unhide_flashcard(self):
267 self.flashcard.hide_from(self.user)
268
269 response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json')
270 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
271
272 response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json')
273
274 # This should fail because the user is not enrolled in section id 2
275 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
276
277
class SectionViewSetTest(APITestCase): 197 278 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 198 279 fixtures = ['testusers', 'testsections']
199 280
def setUp(self): 200 281 def setUp(self):
self.client.login(email='none@none.com', password='1234') 201 282 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 202 283 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user) 203 284 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(),
285 author=self.user)
self.flashcard.save() 204 286 self.flashcard.save()
self.section = Section.objects.get(pk=1) 205 287 self.section = Section.objects.get(pk=1)
206 288
def test_list_sections(self): 207 289 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 208 290 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 209 291 self.assertEqual(response.status_code, HTTP_200_OK)
210 292
def test_section_enroll(self): 211 293 def test_section_enroll(self):
section = self.section 212 294 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 213 295 self.assertFalse(self.user.sections.filter(pk=section.pk))
214 296
# test enrolling in a section without a whitelist 215 297 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 216 298 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 217 299 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 218 300 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
219 301
section = Section.objects.get(pk=2) 220 302 section = Section.objects.get(pk=2)
WhitelistedAddress.objects.create(email='bad@none.com', section=section) 221 303 WhitelistedAddress.objects.create(email='bad@none.com', section=section)
222 304
# test enrolling in a section when not on the whitelist 223 305 # test enrolling in a section when not on the whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 224 306 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 225 307 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
self.assertFalse(self.user.sections.filter(pk=section.pk).exists()) 226 308 self.assertFalse(self.user.sections.filter(pk=section.pk).exists())
227 309
WhitelistedAddress.objects.create(email=self.user.email, section=section) 228 310 WhitelistedAddress.objects.create(email=self.user.email, section=section)
229 311
# test enrolling in a section when on the whitelist 230 312 # test enrolling in a section when on the whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 231 313 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 232 314 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 233 315 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
flashcards/tests/test_models.py View file @ 5769450
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard, UserFlashcard 4 4 from flashcards.models import User, Section, Flashcard, UserFlashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
6 6
7 7
class RegistrationTests(TestCase): 8 8 class RegistrationTests(TestCase):
def setUp(self): 9 9 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 10 10 User.objects.create_user(email="none@none.com", password="1234")
11 11
def test_email_confirmation(self): 12 12 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 13 13 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 14 14 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 15 15 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 16 16 self.assertTrue(user.is_confirmed)
17 17
18 18
class UserTests(TestCase): 19 19 class UserTests(TestCase):
def setUp(self): 20 20 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 21 21 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 22 22 Section.objects.create(department='dept',
course_num='101a', 23 23 course_num='101a',
course_title='how 2 test', 24 24 course_title='how 2 test',
instructor='George Lucas', 25 25 instructor='George Lucas',
quarter='SP15') 26 26 quarter='SP15')
27 27
def test_section_list(self): 28 28 def test_section_list(self):
section = Section.objects.get(course_num='101a') 29 29 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 30 30 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 31 31 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 32 32 user.sections.add(section)
self.assertIn(section, user.sections.all()) 33 33 self.assertIn(section, user.sections.all())
user.sections.add(section) 34 34 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 35 35 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 36 36 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 37 37 self.assertEqual(user.sections.count(), 0)
38 38
39 39
class FlashcardMaskTest(TestCase): 40 40 class FlashcardMaskTest(TestCase):
41 def test_empty(self):
42 try:
43 fm = FlashcardMask([])
44 self.assertEqual(fm.max_offset(), -1)
45 except TypeError:
46 self.fail()
47 try:
48 fm = FlashcardMask('')
49 self.assertEqual(fm.max_offset(), -1)
50 except TypeError:
51 self.fail()
52 try:
53 fm = FlashcardMask(None)
54 self.assertEqual(fm.max_offset(), -1)
55 except TypeError:
56 self.fail()
57
def test_iterable(self): 41 58 def test_iterable(self):
try: 42 59 try:
FlashcardMask(1) 43 60 FlashcardMask(1)
except TypeError as te: 44 61 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 45 62 self.assertEqual(te.message, "Interval not a valid iterable")
try: 46 63 try:
FlashcardMask([1, 2, 4]) 47 64 FlashcardMask([1, 2, 4])
except TypeError as te: 48 65 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 49 66 self.assertEqual(te.message, "Interval not a valid iterable")
50 67
def test_interval(self): 51 68 def test_interval(self):
try: 52 69 try:
FlashcardMask([[1, 2, 3], [1]]) 53 70 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 54 71 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 55 72 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 56 73 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 57 74 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 58 75 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 59 76 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 60 77 try:
FlashcardMask(([1, 2], [1])) 61 78 FlashcardMask(([1, 2], [1]))
except TypeError as te: 62 79 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 63 80 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 64 81 try:
FlashcardMask("[1,2,3]") 65 82 FlashcardMask("[1,2,3]")
except TypeError as te: 66 83 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 67 84 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
68 85
def test_overlap(self): 69 86 def test_overlap(self):
try: 70 87 try:
FlashcardMask({(1, 2), (2, 5)}) 71 88 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 72 89 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 73 90 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 74 91 try:
FlashcardMask({(1, 20), (12, 15)}) 75 92 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 76 93 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 77 94 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 78 95 try:
FlashcardMask({(2, 1), (5, 2)}) 79 96 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 80 97 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 81 98 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
82 99
83 100
84 101
class FlashcardTests(TestCase): 85 102 class FlashcardTests(TestCase):
def setUp(self): 86 103 def setUp(self):
section = Section.objects.create(department='dept', 87 104 section = Section.objects.create(department='dept',
course_num='101a', 88 105 course_num='101a',
course_title='how 2 test', 89 106 course_title='how 2 test',
instructor='George Lucas', 90 107 instructor='George Lucas',
quarter='SP15') 91 108 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 92 109 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 93 110 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 94 111 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 95 112 section=section,
author=user, 96 113 author=user,
material_date=datetime.now(), 97 114 material_date=datetime.now(),
previous=None, 98 115 previous=None,
mask={(24,34), (0, 4)}) 99 116 mask={(24,34), (0, 4)})
user.save() 100 117 user.save()
section.save() 101 118 section.save()
flashcard.save() 102 119 flashcard.save()
103 120
def test_flashcard_edit(self): 104 121 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 105 122 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 106 123 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 107 124 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 108 125 user2.sections.add(section)
user2.save() 109 126 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 110 127 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 111 128 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 112 129 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 113 130 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 114 131 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 115 132 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 116 133 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 117 134 self.assertNotEqual(flashcard.pk, pk_backup)
flashcards/validators.py View file @ 5769450
from collections import Iterable 1 1 from collections import Iterable
2 2
3 3
class FlashcardMask(set): 4 4 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 5 5 def __init__(self, iterable, *args, **kwargs):
6 if iterable is None or iterable == '':
7 iterable = []
self._iterable_check(iterable) 6 8 self._iterable_check(iterable)
iterable = map(tuple, iterable) 7 9 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 8 10 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 9 11 self._interval_check()
self._overlap_check() 10 12 self._overlap_check()
11 13
def max_offset(self): 12 14 def max_offset(self):
return self._end 13 15 return self._end
14 16
def _iterable_check(self, iterable): 15 17 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 16 18 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 17 19 raise TypeError("Interval not a valid iterable")
18 20
def _interval_check(self): 19 21 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 20 22 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 21 23 raise TypeError("Intervals must have exactly 2 elements, begin and end")
22 24
def _overlap_check(self): 23 25 def _overlap_check(self):
p_beg, p_end = -1, -1 24 26 p_beg, p_end = -1, -1
for interval in sorted(self): 25 27 for interval in sorted(self):
beg, end = map(int, interval) 26 28 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 27 29 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") 28 30 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 29 31 p_beg, p_end = beg, end
self._end = p_end 30 32 self._end = p_end
flashcards/views.py View file @ 5769450
1 import django
2
from django.contrib import auth 1 3 from django.contrib import auth
from flashcards.api import StandardResultsSetPagination 2 4 from django.core.cache import cache
5 from django.shortcuts import get_object_or_404
6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 3 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 4 8 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 5 9 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer 6 10 FlashcardUpdateSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view 7 11 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 8 12 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 9 13 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 10 14 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 15 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 16 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 17 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 18 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 19 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 16 20 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 21 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 22 from simple_email_confirmation import EmailAddress
19 23
20 24
class SectionViewSet(ReadOnlyModelViewSet): 21 25 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 26 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 27 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 24 28 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 29 permission_classes = [IsAuthenticated]
26 30
@detail_route(methods=['GET']) 27 31 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 28 32 def flashcards(self, request, pk):
""" 29 33 """
Gets flashcards for a section, excluding hidden cards. 30 34 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 31 35 Returned in strictly chronological order (material date).
""" 32 36 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 33 37 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
section=self.get_object()).all() 34
return Response(FlashcardSerializer(flashcards, many=True).data) 35 38 return Response(FlashcardSerializer(flashcards, many=True).data)
36 39
@detail_route(methods=['post']) 37 40 @detail_route(methods=['post'])
def enroll(self, request, pk): 38 41 def enroll(self, request, pk):
""" 39 42 """
Add the current user to a specified section 40 43 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. 41 44 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 42 45 ---
omit_serializer: true 43 46 view_mocker: flashcards.api.mock_no_params
parameters: 44
- fake: None 45
parameters_strategy: 46
form: replace 47
""" 48 47 """
section = self.get_object() 49 48
if request.user.sections.filter(pk=section.pk).exists(): 50 49 self.get_object().enroll(request.user)
raise ValidationError("You are already in this section.") 51
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 52
raise PermissionDenied("You must be on the whitelist to add this section.") 53
request.user.sections.add(section) 54
return Response(status=HTTP_204_NO_CONTENT) 55 50 return Response(status=HTTP_204_NO_CONTENT)
56 51
@detail_route(methods=['post']) 57 52 @detail_route(methods=['post'])
def drop(self, request, pk): 58 53 def drop(self, request, pk):
""" 59 54 """
Remove the current user from a specified section 60 55 Remove the current user from a specified section
If the user is not in the class, the request will fail. 61 56 If the user is not in the class, the request will fail.
--- 62 57 ---
omit_serializer: true 63 58 view_mocker: flashcards.api.mock_no_params
parameters: 64
- fake: None 65
parameters_strategy: 66
form: replace 67
""" 68 59 """
section = self.get_object() 69 60 try:
if not section.user_set.filter(pk=request.user.pk).exists(): 70 61 self.get_object().drop(request.user)
raise ValidationError("You are not in the section.") 71 62 except django.core.exceptions.PermissionDenied as e:
section.user_set.remove(request.user) 72 63 raise PermissionDenied(e)
64 except django.core.exceptions.ValidationError as e:
65 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 73 66 return Response(status=HTTP_204_NO_CONTENT)
74 67
@detail_route(methods=['GET']) 75 68 @list_route(methods=['GET'])
def search(self, request): 76 69 def search(self, request):
""" 77 70 """
Returns a list of sections which match a user's query 78 71 Returns a list of sections which match a user's query
72 ---
73 parameters:
74 - name: q
75 description: space-separated list of terms
76 required: true
77 type: form
78 response_serializer: SectionSerializer
""" 79 79 """
query = request.GET.get('q', None) 80 80 query = request.GET.get('q', None)
if not query: return Response('[]') 81 81 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 82 82 qs = Section.search(query.split(' '))[:20]
serializer = SectionSerializer(qs, many=True) 83 83 data = SectionSerializer(qs, many=True).data
return Response(serializer.data) 84 84 return Response(data)
85 85
@detail_route(methods=['GET']) 86 86 @detail_route(methods=['GET'])
def deck(self, request, pk): 87 87 def deck(self, request, pk):
""" 88 88 """
Gets the contents of a user's deck for a given section. 89 89 Gets the contents of a user's deck for a given section.
""" 90 90 """
qs = request.user.get_deck(self.get_object()) 91 91 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 92 92 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 93 93 return Response(serializer.data)
94 94
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 95 95 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 96 96 def ordered_deck(self, request, pk):
""" 97 97 """
Get a chronological order by material_date of flashcards for a section. 98 98 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 99 99 This excludes hidden card.
""" 100 100 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 101 101 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 102 102 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 103 103 return Response(serializer.data)
104 104
@detail_route(methods=['GET']) 105 105 @detail_route(methods=['GET'])
def feed(self, request, pk): 106 106 def feed(self, request, pk):
""" 107 107 """
Gets the contents of a user's feed for a section. 108 108 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 109 109 Exclude cards that are already in the user's deck
""" 110 110 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 111 111 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 112 112 return Response(serializer.data)
113 113
114 114
class UserSectionListView(ListAPIView): 115 115 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 116 116 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 117 117 permission_classes = [IsAuthenticated]
118 118
def get_queryset(self): 119 119 def get_queryset(self):
return self.request.user.sections.all() 120 120 return self.request.user.sections.all()
121 121
def paginate_queryset(self, queryset): return None 122 122 def paginate_queryset(self, queryset): return None
123 123
124 124
class UserDetail(GenericAPIView): 125 125 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 126 126 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 127 127 permission_classes = [IsAuthenticated]
128 128
def get_queryset(self): 129
return User.objects.all() 130
131
def patch(self, request, format=None): 132 129 def patch(self, request, format=None):
""" 133 130 """
Updates the user's password, or verifies their email address 134 131 Updates the user's password, or verifies their email address
--- 135 132 ---
request_serializer: UserUpdateSerializer 136 133 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 137 134 response_serializer: UserSerializer
""" 138 135 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 139 136 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 140 137 data.is_valid(raise_exception=True)
data = data.validated_data 141 138 data = data.validated_data
142 139
if 'new_password' in data: 143 140 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 144 141 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 145 142 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 146 143 request.user.set_password(data['new_password'])
request.user.save() 147 144 request.user.save()
148 145
if 'confirmation_key' in data: 149 146 if 'confirmation_key' in data:
try: 150 147 try:
request.user.confirm_email(data['confirmation_key']) 151 148 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 152 149 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 153 150 raise ValidationError('confirmation_key is invalid')
154 151
return Response(UserSerializer(request.user).data) 155 152 return Response(UserSerializer(request.user).data)
156 153
def get(self, request, format=None): 157 154 def get(self, request, format=None):
""" 158 155 """
Return data about the user 159 156 Return data about the user
--- 160 157 ---
response_serializer: UserSerializer 161 158 response_serializer: UserSerializer
""" 162 159 """
serializer = UserSerializer(request.user, context={'request': request}) 163 160 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 164 161 return Response(serializer.data)
165 162
def delete(self, request): 166 163 def delete(self, request):
""" 167 164 """
Irrevocably delete the user and their data 168 165 Irrevocably delete the user and their data
169 166
Yes, really 170 167 Yes, really
""" 171 168 """
request.user.delete() 172 169 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 173 170 return Response(status=HTTP_204_NO_CONTENT)
174 171
175 172
@api_view(['POST']) 176 173 @api_view(['POST'])
def register(request, format=None): 177 174 def register(request, format=None):
""" 178 175 """
Register a new user 179 176 Register a new user
--- 180 177 ---
request_serializer: EmailPasswordSerializer 181 178 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 182 179 response_serializer: UserSerializer
""" 183 180 """
data = RegistrationSerializer(data=request.data) 184 181 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 185 182 data.is_valid(raise_exception=True)
186 183
User.objects.create_user(**data.validated_data) 187 184 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 188 185 user = authenticate(**data.validated_data)
auth.login(request, user) 189 186 auth.login(request, user)
190 187
body = ''' 191
Visit the following link to confirm your email address: 192
https://flashy.cards/app/verifyemail/%s 193
194
If you did not register for Flashy, no action is required. 195
''' 196
197
assert send_mail("Flashy email verification", 198
body % user.confirmation_key, 199
"noreply@flashy.cards", 200
[user.email]) 201
202
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 203 188 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
204 189
205 190
@api_view(['POST']) 206 191 @api_view(['POST'])
def login(request): 207 192 def login(request):
""" 208 193 """
Authenticates user and returns user data if valid. 209 194 Authenticates user and returns user data if valid.
--- 210 195 ---
request_serializer: EmailPasswordSerializer 211 196 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 212 197 response_serializer: UserSerializer
""" 213 198 """
214 199
data = EmailPasswordSerializer(data=request.data) 215 200 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 216 201 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 217 202 user = authenticate(**data.validated_data)
218 203
if user is None: 219 204 if user is None:
raise AuthenticationFailed('Invalid email or password') 220 205 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 221 206 if not user.is_active:
raise NotAuthenticated('Account is disabled') 222 207 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 223 208 auth.login(request, user)
return Response(UserSerializer(request.user).data) 224 209 return Response(UserSerializer(request.user).data)
225 210
226 211
@api_view(['POST']) 227 212 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 228 213 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 229 214 def logout(request, format=None):
""" 230 215 """
Logs the authenticated user out. 231 216 Logs the authenticated user out.
""" 232 217 """
auth.logout(request) 233 218 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 234 219 return Response(status=HTTP_204_NO_CONTENT)
235 220
236 221
@api_view(['POST']) 237 222 @api_view(['POST'])
def request_password_reset(request, format=None): 238 223 def request_password_reset(request, format=None):
""" 239 224 """
Send a password reset token/link to the provided email. 240 225 Send a password reset token/link to the provided email.
--- 241 226 ---
request_serializer: PasswordResetRequestSerializer 242 227 request_serializer: PasswordResetRequestSerializer
""" 243 228 """
data = PasswordResetRequestSerializer(data=request.data) 244 229 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 230 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 246 231 get_object_or_404(User, email=data['email'].value).request_password_reset()
token = default_token_generator.make_token(user) 247
248
body = ''' 249
Visit the following link to reset your password: 250
https://flashy.cards/app/resetpassword/%d/%s 251
252
If you did not request a password reset, no action is required. 253
''' 254
255
send_mail("Flashy password reset", 256
body % (user.pk, token), 257
"noreply@flashy.cards", 258
[user.email]) 259
260
return Response(status=HTTP_204_NO_CONTENT) 261 232 return Response(status=HTTP_204_NO_CONTENT)
262 233
263 234
@api_view(['POST']) 264 235 @api_view(['POST'])
def reset_password(request, format=None): 265 236 def reset_password(request, format=None):
""" 266 237 """
Updates user's password to new password if token is valid. 267 238 Updates user's password to new password if token is valid.
--- 268 239 ---
request_serializer: PasswordResetSerializer 269 240 request_serializer: PasswordResetSerializer
""" 270 241 """
data = PasswordResetSerializer(data=request.data) 271 242 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 272 243 data.is_valid(raise_exception=True)
273 244
user = User.objects.get(id=data['uid'].value) 274 245 user = User.objects.get(id=data['uid'].value)
# Check token validity. 275 246 # Check token validity.
276 247
if default_token_generator.check_token(user, data['token'].value): 277 248 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 278 249 user.set_password(data['new_password'].value)
user.save() 279 250 user.save()
else: 280 251 else:
raise ValidationError('Could not verify reset token') 281 252 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 282 253 return Response(status=HTTP_204_NO_CONTENT)
283 254
284 255
class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): 285 256 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 286 257 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 287 258 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 288 259 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
289 260
# Override create in CreateModelMixin 290 261 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 291 262 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 292 263 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 293 264 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 294 265 data = serializer.validated_data
self.perform_create(serializer) 295 266 if not request.user.is_in_section(data['section']):
headers = self.get_success_headers(serializer.data) 296 267 raise PermissionDenied('The user is not enrolled in that section')
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 297 268 data['author'] = request.user
269 flashcard = Flashcard.objects.create(**data)
270 self.perform_create(flashcard)
271 headers = self.get_success_headers(data)
272 response_data = FlashcardSerializer(flashcard)
273 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
298 274
275
@detail_route(methods=['post']) 299 276 @detail_route(methods=['post'])
277 def unhide(self, request, pk):
278 """
279 Unhide the given card
280 ---
281 view_mocker: flashcards.api.mock_no_params
282 """
283 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
284 hide.delete()
285 return Response(status=HTTP_204_NO_CONTENT)
286
287 @detail_route(methods=['post'])
def report(self, request, pk): 300 288 def report(self, request, pk):
""" 301 289 """
Report the given card 302 290 Hide the given card
--- 303 291 ---
omit_serializer: true 304 292 view_mocker: flashcards.api.mock_no_params
parameters: 305
- fake: None 306
parameters_strategy: 307
form: replace 308
""" 309 293 """
obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) 310 294 self.get_object().report(request.user)
obj.reason = request.data['reason'] 311
if created: 312
obj.save() 313
return Response(status=HTTP_204_NO_CONTENT) 314 295 return Response(status=HTTP_204_NO_CONTENT)
315 296
297 hide = report
298
@detail_route(methods=['POST']) 316 299 @detail_route(methods=['POST'])
def pull(self, request, pk): 317 300 def pull(self, request, pk):
""" 318 301 """
Pull a card from the live feed into the user's deck. 319 302 Pull a card from the live feed into the user's deck.
:param request: The request object 320 303 ---
:param pk: The primary key 321 304 view_mocker: flashcards.api.mock_no_params
:return: A 204 response upon success. 322
""" 323 305 """
flashcard = self.get_object() 324 306 flashcard = self.get_object()
user.unpull(flashcard) 325 307 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 326 308 return Response(status=HTTP_204_NO_CONTENT)
327 309
@detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) 328 310 @detail_route(methods=['POST'])
def unpull(self, request, pk): 329 311 def unpull(self, request, pk):
""" 330 312 """
TODO: delete a flashcard from the user's deck 331 313 Unpull a card from the user's deck
314 ---
315 view_mocker: flashcards.api.mock_no_params
""" 332 316 """
317 user = request.user
flashcard = self.get_object() 333 318 flashcard = self.get_object()
if flashcard.userFlashcard: 334 319 user.unpull(flashcard)
flashcard.userFlashcard.delete() 335
else: 336
raise ValidationError('You do not have this flashcard in your deck.') 337
return Response(status=HTTP_204_NO_CONTENT) 338 320 return Response(status=HTTP_204_NO_CONTENT)
339 321
340 322 def partial_update(self, request, *args, **kwargs):
@detail_route(methods=['PATCH'], permission_classes=[IsAuthenticated]) 341
def update(self, request, *args, **kwargs): 342
""" 343 323 """
Edit settings related to a card for the user. 344 324 Edit settings related to a card for the user.
:param request: The request object. 345 325 ---
:param pk: The primary key of the flashcard. 346 326 request_serializer: FlashcardUpdateSerializer
:return: A 204 response upon success. 347
flashy/settings.py View file @ 5769450
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 1 1 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os 2 2 import os
3 3
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 4 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 5
IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ 6 6 IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ
7 7
DEBUG = not IN_PRODUCTION 8 8 DEBUG = not IN_PRODUCTION
9 9
ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] 10 10 ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
11 11
AUTH_USER_MODEL = 'flashcards.User' 12 12 AUTH_USER_MODEL = 'flashcards.User'
REST_FRAMEWORK = { 13 13 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 14 14 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20 15 15 'PAGE_SIZE': 20
} 16 16 }
INSTALLED_APPS = [ 17 17 INSTALLED_APPS = [
'simple_email_confirmation', 18 18 'simple_email_confirmation',
'flashcards', 19 19 'flashcards',
'django.contrib.admin', 20 20 'django.contrib.admin',
'django.contrib.admindocs', 21 21 'django.contrib.admindocs',
'django.contrib.auth', 22 22 'django.contrib.auth',
'django.contrib.contenttypes', 23 23 'django.contrib.contenttypes',
'django.contrib.sessions', 24 24 'django.contrib.sessions',
'django.contrib.messages', 25 25 'django.contrib.messages',
'django.contrib.staticfiles', 26 26 'django.contrib.staticfiles',
27 27
'rest_framework_swagger', 28 28 'rest_framework_swagger',
'rest_framework', 29 29 'rest_framework',
] 30 30 ]
31 31
MIDDLEWARE_CLASSES = ( 32 32 MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 33 33 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 34 34 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 35 35 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 36 36 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 37 37 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 38 38 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 39 39 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 40 40 'django.middleware.security.SecurityMiddleware',
) 41 41 )
42 42
ROOT_URLCONF = 'flashy.urls' 43 43 ROOT_URLCONF = 'flashy.urls'
44 44
AUTHENTICATION_BACKENDS = ( 45 45 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 46 46 'django.contrib.auth.backends.ModelBackend',
) 47 47 )
48 48
TEMPLATES = [ 49 49 TEMPLATES = [
{ 50 50 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 51 51 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 52 52 'DIRS': ['templates/'],
'APP_DIRS': True, 53 53 'APP_DIRS': True,
'OPTIONS': { 54 54 'OPTIONS': {
'context_processors': [ 55 55 'context_processors': [
'django.template.context_processors.debug', 56 56 'django.template.context_processors.debug',
'django.template.context_processors.request', 57 57 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 58 58 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 59 59 'django.contrib.messages.context_processors.messages',
], 60 60 ],
}, 61 61 },
}, 62 62 },
] 63 63 ]
64 64
WSGI_APPLICATION = 'flashy.wsgi.application' 65 65 WSGI_APPLICATION = 'flashy.wsgi.application'
66 66
DATABASES = { 67 67 DATABASES = {
'default': { 68 68 'default': {
'ENGINE': 'django.db.backends.sqlite3', 69 69 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 70 70 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 71 71 }
} 72 72 }
73 73
if IN_PRODUCTION: 74 74 if IN_PRODUCTION:
DATABASES['default'] = { 75 75 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 76 76 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 77 77 'NAME': 'flashy',
'USER': 'flashy', 78 78 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 79 79 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 80 80 'HOST': 'localhost',
'PORT': '', 81 81 'PORT': '',
} 82 82 }
83 83
LANGUAGE_CODE = 'en-us' 84 84 LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles' 85 85 TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True 86 86 USE_I18N = True
USE_L10N = True 87 87 USE_L10N = True
USE_TZ = True 88 88 USE_TZ = True
89 89
STATIC_URL = '/static/' 90 90 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 91 91 STATIC_ROOT = 'static'
92 92
# Four settings just to be sure 93 93 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 94 94 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 95 95 EMAIL_HOST_USER = 'noreply@flashy.cards'
DEFAULT_FROM_EMAIL = 'noreply@flashy.cards' 96 96 DEFAULT_FROM_EMAIL = 'noreply@flashy.cards'
SERVER_EMAIL = 'noreply@flashy.cards' 97 97 SERVER_EMAIL = 'noreply@flashy.cards'
98 98
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 99 99 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
100 100
if IN_PRODUCTION: 101 101 if IN_PRODUCTION:
INSTALLED_APPS.append('django_ses') 102 102 INSTALLED_APPS.append('django_ses')
AWS_SES_REGION_NAME = 'us-west-2' 103 103 AWS_SES_REGION_NAME = 'us-west-2'
AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com' 104 104 AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com'
EMAIL_BACKEND = 'django_ses.SESBackend' 105 105 EMAIL_BACKEND = 'django_ses.SESBackend'
106 106
if IN_PRODUCTION: 107 107 if IN_PRODUCTION:
SESSION_COOKIE_SECURE = True 108 108 SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True 109 109 CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 110 110 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# are we secure yet? 111 111 # are we secure yet?
112 112
if IN_PRODUCTION: 113 113 if IN_PRODUCTION:
LOGGING = { 114 114 LOGGING = {
'version': 1, 115 115 'version': 1,
'disable_existing_loggers': False, 116 116 'disable_existing_loggers': False,
'handlers': { 117 117 'handlers': {
'file': { 118 118 'file': {
'level': 'DEBUG', 119 119 'level': 'DEBUG',
'class': 'logging.FileHandler', 120 120 'class': 'logging.FileHandler',
'filename': 'debug.log', 121 121 'filename': 'debug.log',
flashy/urls.py View file @ 5769450
from django.conf.urls import include, url 1 1 from django.conf.urls import include, url
from django.contrib import admin 2 2 from django.contrib import admin
from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ 3 3 from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \
reset_password, logout, login, register 4 4 reset_password, logout, login, register
from flashy.frontend_serve import serve_with_default 5 5 from flashy.frontend_serve import serve_with_default
from flashy.settings import DEBUG, IN_PRODUCTION 6 6 from flashy.settings import DEBUG, IN_PRODUCTION
from rest_framework.routers import DefaultRouter 7 7 from rest_framework.routers import DefaultRouter
from flashcards.api import * 8 8 from flashcards.api import *
9 9
router = DefaultRouter() 10 10 router = DefaultRouter()
router.register(r'sections', SectionViewSet) 11 11 router.register(r'sections', SectionViewSet)
router.register(r'flashcards', FlashcardViewSet) 12 12 router.register(r'flashcards', FlashcardViewSet)
13 13
urlpatterns = [ 14 14 urlpatterns = [
url(r'^api/docs/', include('rest_framework_swagger.urls')), 15 15 url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api/me$', UserDetail.as_view()), 16 16 url(r'^api/me/$', UserDetail.as_view()),
url(r'^api/register', register), 17 17 url(r'^api/register/', register),
url(r'^api/login$', login), 18 18 url(r'^api/login/$', login),
url(r'^api/logout$', logout), 19 19 url(r'^api/logout/$', logout),
url(r'^api/me/sections', UserSectionListView.as_view()), 20 20 url(r'^api/me/sections/', UserSectionListView.as_view()),
url(r'^api/request_password_reset', request_password_reset), 21 21 url(r'^api/request_password_reset/', request_password_reset),
url(r'^api/reset_password', reset_password), 22 22 url(r'^api/reset_password/', reset_password),
url(r'^api/', include(router.urls)), 23 23 url(r'^api/', include(router.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 24 24 url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)), 25 25 url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 26 26 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
] 27 27 ]
28 28
if IN_PRODUCTION: 29 29 if IN_PRODUCTION:
urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) 30 30 urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),)
31 31
if DEBUG: 32 32 if DEBUG:
urlpatterns += [url(r'^app/(?P<path>.*)$', serve_with_default, 33 33 urlpatterns += [url(r'^app/(?P<path>.*)$', serve_with_default,
{'document_root': '../flashy-frontend', 'default_file': 'home.html'})] 34 34 {'document_root': '../flashy-frontend', 'default_file': 'home.html'})]
35 35
36 36