Commit 5769450a64e1fb8413aca028936ff578ebe18fd4
Exists in
master
trying to pull
Showing 15 changed files Side-by-side Diff
- README.md
- flashcards/api.py
- flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py
- flashcards/migrations/0013_auto_20150516_2356.py
- flashcards/migrations/0013_auto_20150517_0402.py
- flashcards/migrations/0014_merge.py
- flashcards/migrations/0015_auto_20150518_0017.py
- flashcards/models.py
- flashcards/serializers.py
- flashcards/tests/test_api.py
- flashcards/tests/test_models.py
- flashcards/validators.py
- flashcards/views.py
- flashy/settings.py
- flashy/urls.py
README.md
View file @
5769450
flashcards/api.py
View file @
5769450
1 | +from flashcards.models import Flashcard | |
1 | 2 | from rest_framework.pagination import PageNumberPagination |
2 | 3 | from rest_framework.permissions import BasePermission |
3 | 4 | |
4 | 5 | |
6 | +mock_no_params = lambda x:None | |
7 | + | |
5 | 8 | class StandardResultsSetPagination(PageNumberPagination): |
6 | 9 | page_size = 40 |
7 | 10 | page_size_query_param = 'page_size' |
8 | 11 | |
... | ... | @@ -12,8 +15,15 @@ |
12 | 15 | """ |
13 | 16 | Permissions for the user detail view. Anonymous users may only POST. |
14 | 17 | """ |
18 | + | |
15 | 19 | def has_object_permission(self, request, view, obj): |
16 | 20 | if request.method == 'POST': |
17 | 21 | return True |
18 | 22 | return request.user.is_authenticated() |
23 | + | |
24 | + | |
25 | +class IsEnrolledInAssociatedSection(BasePermission): | |
26 | + def has_object_permission(self, request, view, obj): | |
27 | + assert type(obj) is Flashcard | |
28 | + return request.user.is_in_section(obj.section) |
flashcards/migrations/0001_squashed_0015_auto_20150518_0017.py
View file @
5769450
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 | + ), | |
352 | + migrations.AlterField( | |
353 | + model_name='flashcard', | |
354 | + name='hide_reason', | |
355 | + field=models.CharField(default=b'', max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True), | |
356 | + ), | |
357 | + ] |
flashcards/migrations/0013_auto_20150516_2356.py
View file @
5769450
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( | |
20 | + model_name='flashcard', | |
21 | + name='previous', | |
22 | + field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), | |
23 | + ), | |
24 | + ] |
flashcards/migrations/0013_auto_20150517_0402.py
View file @
5769450
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( | |
20 | + model_name='flashcard', | |
21 | + name='previous', | |
22 | + field=models.ForeignKey(default=None, blank=True, to='flashcards.Flashcard', help_text=b'The previous version of this card, if one exists', null=True), | |
23 | + ), | |
24 | + ] |
flashcards/migrations/0014_merge.py
View file @
5769450
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'), | |
11 | + ('flashcards', '0013_auto_20150516_2356'), | |
12 | + ] | |
13 | + | |
14 | + operations = [ | |
15 | + ] |
flashcards/migrations/0015_auto_20150518_0017.py
View file @
5769450
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( | |
15 | + model_name='flashcard', | |
16 | + name='hide_reason', | |
17 | + field=models.CharField(default=b'', max_length=255, null=True, help_text=b'Reason for hiding this card', blank=True), | |
18 | + ), | |
19 | + ] |
flashcards/models.py
View file @
5769450
1 | -from datetime import datetime | |
2 | - | |
3 | 1 | from django.contrib.auth.models import AbstractUser, UserManager |
4 | -from django.core.exceptions import PermissionDenied | |
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 | |
5 | 8 | from django.db.models import * |
6 | 9 | from django.utils.timezone import now |
7 | 10 | from simple_email_confirmation import SimpleEmailConfirmationUserMixin |
8 | 11 | |
... | ... | @@ -33,8 +36,20 @@ |
33 | 36 | return user |
34 | 37 | |
35 | 38 | def create_user(self, email, password=None, **extra_fields): |
36 | - return self._create_user(email, password, False, False, **extra_fields) | |
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 | + | |
38 | 53 | def create_superuser(self, email, password, **extra_fields): |
39 | 54 | return self._create_user(email, password, True, True, **extra_fields) |
40 | 55 | |
41 | 56 | |
42 | 57 | |
43 | 58 | |
44 | 59 | |
... | ... | @@ -56,20 +71,41 @@ |
56 | 71 | if not self.is_in_section(flashcard.section): |
57 | 72 | raise ValueError("User not in the section this flashcard belongs to") |
58 | 73 | user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) |
59 | - user_card.pulled = datetime.now() | |
74 | + user_card.pulled = now() | |
60 | 75 | user_card.save() |
61 | 76 | |
62 | 77 | def unpull(self, flashcard): |
63 | - user = self | |
64 | - result = user.userflashcard_set.filter(flashcard=self) | |
65 | - if not result.exists(): raise ValidationError ('You cannot remove this flashcard.') | |
78 | + if not self.is_in_section(flashcard.section): | |
79 | + raise ValueError("User not in the section this flashcard belongs to") | |
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 | + | |
67 | 88 | def get_deck(self, section): |
68 | 89 | if not self.is_in_section(section): |
69 | 90 | raise ObjectDoesNotExist("User not enrolled in 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 | + | |
73 | 109 | class UserFlashcard(Model): |
74 | 110 | """ |
75 | 111 | Represents the relationship between a user and a flashcard by: |
76 | 112 | |
... | ... | @@ -114,11 +150,12 @@ |
114 | 150 | section = ForeignKey('Section', help_text='The section with which the card is associated') |
115 | 151 | pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") |
116 | 152 | material_date = DateTimeField(default=now, help_text="The date with which the card is associated") |
117 | - previous = ForeignKey('Flashcard', null=True, blank=True, | |
153 | + previous = ForeignKey('Flashcard', null=True, blank=True, default=None, | |
118 | 154 | help_text="The previous version of this card, if one exists") |
119 | 155 | author = ForeignKey(User) |
120 | 156 | is_hidden = BooleanField(default=False) |
121 | - hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") | |
157 | + hide_reason = CharField(blank=True, null=True, max_length=255, default='', | |
158 | + help_text="Reason for hiding this card") | |
122 | 159 | mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") |
123 | 160 | |
124 | 161 | class Meta: |
125 | 162 | |
126 | 163 | |
127 | 164 | |
... | ... | @@ -132,19 +169,37 @@ |
132 | 169 | :param user: |
133 | 170 | :return: Whether the card is hidden from the user. |
134 | 171 | """ |
135 | - result = user.userflashcard_set.filter(flashcard=self) | |
136 | - if not result.exists(): return self.is_hidden | |
137 | - return result[0].is_hidden() | |
172 | + if self.userflashcard_set.filter(user=user).exists(): return False | |
173 | + if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True | |
174 | + return False | |
138 | 175 | |
139 | - def edit(self, user, new_flashcard): | |
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 | """ |
141 | 198 | Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. |
142 | 199 | Sets up everything correctly so this object, when saved, will result in the appropriate changes. |
143 | 200 | :param user: The user editing this card. |
144 | - :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. | |
201 | + :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. | |
145 | 202 | """ |
146 | - if not user.is_in_section(self.section): | |
147 | - raise PermissionDenied("You don't have the permission to edit this card") | |
148 | 203 | |
149 | 204 | # content_changed is True iff either material_date or text were changed |
150 | 205 | content_changed = False |
151 | 206 | |
152 | 207 | |
153 | 208 | |
154 | 209 | |
... | ... | @@ -152,19 +207,30 @@ |
152 | 207 | # and there are no other users with this card in their decks |
153 | 208 | create_new = user != self.author or \ |
154 | 209 | UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() |
155 | - | |
156 | - if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: | |
157 | - content_changed |= True | |
158 | - self.material_date = new_flashcard['material_date'] | |
159 | - if 'text' in new_flashcard and self.text != new_flashcard['text']: | |
160 | - content_changed |= True | |
161 | - self.text = new_flashcard['text'] | |
210 | + if 'material_date' in new_data and self.material_date != new_data['material_date']: | |
211 | + content_changed = True | |
212 | + self.material_date = new_data['material_date'] | |
213 | + if 'text' in new_data and self.text != new_data['text']: | |
214 | + content_changed = True | |
215 | + self.text = new_data['text'] | |
162 | 216 | if create_new and content_changed: |
217 | + if self.is_in_deck(user): user.unpull(self) | |
218 | + self.previous_id = self.pk | |
163 | 219 | self.pk = None |
164 | - if 'mask' in new_flashcard: | |
165 | - self.mask = new_flashcard['mask'] | |
220 | + self.mask = new_data.get('mask', self.mask) | |
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 | + | |
168 | 234 | @classmethod |
169 | 235 | def cards_visible_to(cls, user): |
170 | 236 | """ |
171 | 237 | |
172 | 238 | |
... | ... | @@ -247,15 +313,36 @@ |
247 | 313 | """ |
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 | + | |
250 | 329 | class Meta: |
251 | 330 | ordering = ['-course_title'] |
252 | 331 | |
253 | 332 | @property |
254 | 333 | def lecture_times(self): |
255 | - lecture_periods = self.lectureperiod_set.all() | |
256 | - if not lecture_periods.exists(): return '' | |
257 | - return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time | |
334 | + data = cache.get("section_%d_lecture_times" % self.pk) | |
335 | + if not data: | |
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 | + | |
259 | 346 | @property |
260 | 347 | def long_name(self): |
261 | 348 | return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor) |
... | ... | @@ -268,6 +355,9 @@ |
268 | 355 | qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed') |
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 | + | |
271 | 361 | def __unicode__(self): |
272 | 362 | return '%s %s: %s (%s %s)' % ( |
273 | 363 | self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) |
... | ... | @@ -290,7 +380,8 @@ |
290 | 380 | |
291 | 381 | @property |
292 | 382 | def short_start_time(self): |
293 | - return self.start_time.strftime('%-I %p') | |
383 | + # lstrip 0 because windows doesn't support %-I | |
384 | + return self.start_time.strftime('%I %p').lstrip('0') | |
294 | 385 | |
295 | 386 | class Meta: |
296 | 387 | unique_together = (('section', 'start_time', 'week_day'),) |
flashcards/serializers.py
View file @
5769450
1 | +from json import dumps, loads | |
2 | + | |
1 | 3 | from django.utils.datetime_safe import datetime |
2 | 4 | from django.utils.timezone import now |
3 | 5 | import pytz |
4 | 6 | |
... | ... | @@ -5,10 +7,8 @@ |
5 | 7 | from flashcards.validators import FlashcardMask, OverlapIntervalException |
6 | 8 | from rest_framework import serializers |
7 | 9 | from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField |
8 | -from rest_framework.relations import HyperlinkedRelatedField | |
9 | 10 | from rest_framework.serializers import ModelSerializer, Serializer |
10 | 11 | from rest_framework.validators import UniqueValidator |
11 | -from json import dumps, loads | |
12 | 12 | |
13 | 13 | |
14 | 14 | class EmailSerializer(Serializer): |
... | ... | @@ -69,7 +69,6 @@ |
69 | 69 | |
70 | 70 | |
71 | 71 | class SectionSerializer(ModelSerializer): |
72 | - lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) | |
73 | 72 | lecture_times = CharField() |
74 | 73 | short_name = CharField() |
75 | 74 | long_name = CharField() |
76 | 75 | |
77 | 76 | |
... | ... | @@ -77,10 +76,14 @@ |
77 | 76 | class Meta: |
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 | + | |
81 | 84 | class UserSerializer(ModelSerializer): |
82 | 85 | email = EmailField(required=False) |
83 | - sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') | |
86 | + sections = SectionSerializer(many=True) | |
84 | 87 | is_confirmed = BooleanField() |
85 | 88 | |
86 | 89 | class Meta: |
... | ... | @@ -119,7 +122,7 @@ |
119 | 122 | is_hidden = BooleanField(read_only=True) |
120 | 123 | hide_reason = CharField(read_only=True) |
121 | 124 | material_date = DateTimeField(default=now) |
122 | - mask = MaskFieldSerializer() | |
125 | + mask = MaskFieldSerializer(allow_null=True) | |
123 | 126 | |
124 | 127 | def validate_material_date(self, value): |
125 | 128 | utc = pytz.UTC |
126 | 129 | |
127 | 130 | |
128 | 131 | |
129 | 132 | |
130 | 133 | |
... | ... | @@ -132,45 +135,30 @@ |
132 | 135 | else: |
133 | 136 | raise serializers.ValidationError("Material date is outside allowed range for this quarter") |
134 | 137 | |
135 | - def validate_previous(self, value): | |
136 | - if value is None: | |
137 | - return value | |
138 | - if Flashcard.objects.filter(pk=value).count() > 0: | |
139 | - return value | |
140 | - raise serializers.ValidationError("Invalid previous Flashcard object") | |
141 | - | |
142 | 138 | def validate_pushed(self, value): |
143 | 139 | if value > datetime.now(): |
144 | 140 | raise serializers.ValidationError("Invalid creation date for the Flashcard") |
145 | 141 | return value |
146 | 142 | |
147 | - def validate_text(self, value): | |
148 | - if len(value) > 255: | |
149 | - raise serializers.ValidationError("Flashcard text limit exceeded") | |
150 | - return value | |
151 | - | |
152 | - def validate_hide_reason(self, value): | |
153 | - if len(value) > 255: | |
154 | - raise serializers.ValidationError("Hide reason limit exceeded") | |
155 | - return value | |
156 | - | |
157 | 143 | def validate_mask(self, value): |
158 | - if len(self.data['text']) < value.max_offset(): | |
144 | + if value is None: | |
145 | + return None | |
146 | + if len(self.initial_data['text']) < value.max_offset(): | |
159 | 147 | raise serializers.ValidationError("Mask out of bounds") |
160 | 148 | return value |
161 | 149 | |
162 | 150 | class Meta: |
163 | 151 | model = Flashcard |
164 | - exclude = 'author', | |
152 | + exclude = 'author', 'previous' | |
165 | 153 | |
166 | 154 | |
167 | 155 | class FlashcardUpdateSerializer(serializers.Serializer): |
168 | - text = CharField(max_length=255) | |
169 | - material_date = DateTimeField() | |
170 | - mask = MaskFieldSerializer() | |
156 | + text = CharField(max_length=255, required=False) | |
157 | + material_date = DateTimeField(required=False) | |
158 | + mask = MaskFieldSerializer(required=False) | |
171 | 159 | |
172 | 160 | def validate_material_date(self, date): |
173 | - quarter_end = datetime(2015, 6, 15) | |
161 | + quarter_end = pytz.UTC.localize(datetime(2015, 6, 15)) | |
174 | 162 | if date > quarter_end: |
175 | 163 | raise serializers.ValidationError("Invalid material_date for the flashcard") |
176 | 164 | return date |
flashcards/tests/test_api.py
View file @
5769450
... | ... | @@ -3,14 +3,17 @@ |
3 | 3 | from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN |
4 | 4 | from rest_framework.test import APITestCase |
5 | 5 | from re import search |
6 | +import datetime | |
6 | 7 | from django.utils.timezone import now |
8 | +from flashcards.validators import FlashcardMask | |
9 | +from flashcards.serializers import FlashcardSerializer | |
7 | 10 | |
8 | 11 | |
9 | 12 | class LoginTests(APITestCase): |
10 | 13 | fixtures = ['testusers'] |
11 | 14 | |
12 | 15 | def test_login(self): |
13 | - url = '/api/login' | |
16 | + url = '/api/login/' | |
14 | 17 | data = {'email': 'none@none.com', 'password': '1234'} |
15 | 18 | response = self.client.post(url, data, format='json') |
16 | 19 | self.assertEqual(response.status_code, HTTP_200_OK) |
17 | 20 | |
... | ... | @@ -41,11 +44,11 @@ |
41 | 44 | |
42 | 45 | def test_logout(self): |
43 | 46 | self.client.login(email='none@none.com', password='1234') |
44 | - response = self.client.post('/api/logout') | |
47 | + response = self.client.post('/api/logout/') | |
45 | 48 | self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) |
46 | 49 | |
47 | 50 | # since we're not logged in, we should get a 403 response |
48 | - response = self.client.get('/api/me', format='json') | |
51 | + response = self.client.get('/api/me/', format='json') | |
49 | 52 | self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) |
50 | 53 | |
51 | 54 | |
52 | 55 | |
... | ... | @@ -54,14 +57,14 @@ |
54 | 57 | |
55 | 58 | def test_reset_password(self): |
56 | 59 | # submit the request to reset the password |
57 | - url = '/api/request_password_reset' | |
60 | + url = '/api/request_password_reset/' | |
58 | 61 | post_data = {'email': 'none@none.com'} |
59 | 62 | self.client.post(url, post_data, format='json') |
60 | 63 | self.assertEqual(len(mail.outbox), 1) |
61 | 64 | self.assertIn('reset your password', mail.outbox[0].body) |
62 | 65 | |
63 | 66 | # capture the reset token from the email |
64 | - capture = search('https://flashy.cards/app/reset_password/(\d+)/(.*)', | |
67 | + capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', | |
65 | 68 | mail.outbox[0].body) |
66 | 69 | patch_data = {'new_password': '4321'} |
67 | 70 | patch_data['uid'] = capture.group(1) |
... | ... | @@ -69,7 +72,7 @@ |
69 | 72 | |
70 | 73 | # try to reset the password with the wrong reset token |
71 | 74 | patch_data['token'] = 'wrong_token' |
72 | - url = '/api/reset_password' | |
75 | + url = '/api/reset_password/' | |
73 | 76 | response = self.client.post(url, patch_data, format='json') |
74 | 77 | self.assertContains(response, 'Could not verify reset token', status_code=400) |
75 | 78 | |
... | ... | @@ -83,7 +86,7 @@ |
83 | 86 | |
84 | 87 | class RegistrationTest(APITestCase): |
85 | 88 | def test_create_account(self): |
86 | - url = '/api/register' | |
89 | + url = '/api/register/' | |
87 | 90 | |
88 | 91 | # missing password |
89 | 92 | data = {'email': 'none@none.com'} |
... | ... | @@ -116,7 +119,7 @@ |
116 | 119 | |
117 | 120 | # try activating with an invalid key |
118 | 121 | |
119 | - url = '/api/me' | |
122 | + url = '/api/me/' | |
120 | 123 | response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) |
121 | 124 | self.assertContains(response, 'confirmation_key is invalid', status_code=400) |
122 | 125 | |
... | ... | @@ -129,7 +132,7 @@ |
129 | 132 | fixtures = ['testusers'] |
130 | 133 | |
131 | 134 | def test_get_me(self): |
132 | - url = '/api/me' | |
135 | + url = '/api/me/' | |
133 | 136 | response = self.client.get(url, format='json') |
134 | 137 | # since we're not logged in, we shouldn't be able to see this |
135 | 138 | self.assertEqual(response.status_code, 403) |
136 | 139 | |
... | ... | @@ -139,11 +142,26 @@ |
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 | + | |
142 | 160 | class PasswordChangeTest(APITestCase): |
143 | 161 | fixtures = ['testusers'] |
144 | 162 | |
145 | 163 | def test_change_password(self): |
146 | - url = '/api/me' | |
164 | + url = '/api/me/' | |
147 | 165 | user = User.objects.get(email='none@none.com') |
148 | 166 | self.assertTrue(user.check_password('1234')) |
149 | 167 | |
... | ... | @@ -169,7 +187,7 @@ |
169 | 187 | fixtures = ['testusers'] |
170 | 188 | |
171 | 189 | def test_delete_user(self): |
172 | - url = '/api/me' | |
190 | + url = '/api/me/' | |
173 | 191 | user = User.objects.get(email='none@none.com') |
174 | 192 | |
175 | 193 | self.client.login(email='none@none.com', password='1234') |
176 | 194 | |
177 | 195 | |
178 | 196 | |
179 | 197 | |
180 | 198 | |
181 | 199 | |
... | ... | @@ -181,26 +199,90 @@ |
181 | 199 | fixtures = ['testusers', 'testsections'] |
182 | 200 | |
183 | 201 | def setUp(self): |
184 | - section = Section.objects.get(pk=1) | |
185 | - user = User.objects.get(email='none@none.com') | |
186 | - | |
187 | - self.flashcard = Flashcard(text="jason", section=section, material_date=now(), author=user) | |
202 | + self.section = Section.objects.get(pk=1) | |
203 | + self.user = User.objects.get(email='none@none.com') | |
204 | + self.section.enroll(self.user) | |
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) | |
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 | + | |
190 | 252 | def test_get_flashcard(self): |
191 | - self.client.login(email='none@none.com', password='1234') | |
192 | 253 | response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") |
193 | 254 | self.assertEqual(response.status_code, HTTP_200_OK) |
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 | + | |
197 | 278 | class SectionViewSetTest(APITestCase): |
198 | 279 | fixtures = ['testusers', 'testsections'] |
199 | 280 | |
200 | 281 | def setUp(self): |
201 | 282 | self.client.login(email='none@none.com', password='1234') |
202 | 283 | self.user = User.objects.get(email='none@none.com') |
203 | - self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), author=self.user) | |
284 | + self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), | |
285 | + author=self.user) | |
204 | 286 | self.flashcard.save() |
205 | 287 | self.section = Section.objects.get(pk=1) |
206 | 288 | |
207 | 289 | |
... | ... | @@ -270,10 +352,12 @@ |
270 | 352 | self.assertEqual(response.status_code, HTTP_200_OK) |
271 | 353 | |
272 | 354 | def test_section_feed(self): |
273 | - response = self.client.get('/api/sections/1/feed/') | |
355 | + Flashcard.objects.create(author=self.user, material_date=now(), | |
356 | + text='wow', section=self.section, | |
357 | + mask=None).save() | |
358 | + response = self.client.get('/api/sections/{}/feed/'.format(self.section.pk)) | |
274 | 359 | self.assertEqual(response.status_code, HTTP_200_OK) |
275 | - print response.data | |
276 | - self.assertEqual(response.data, {}) | |
360 | + self.assertEqual(response.data[0]['id'], 1) | |
277 | 361 | |
278 | 362 | def test_section_ordered_deck(self): |
279 | 363 | self.user.sections.add(self.section) |
flashcards/tests/test_models.py
View file @
5769450
... | ... | @@ -38,6 +38,23 @@ |
38 | 38 | |
39 | 39 | |
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 | + | |
41 | 58 | def test_iterable(self): |
42 | 59 | try: |
43 | 60 | FlashcardMask(1) |
... | ... | @@ -115,6 +132,7 @@ |
115 | 132 | UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() |
116 | 133 | flashcard.edit(user2, {'text': 'This is the new text'}) |
117 | 134 | self.assertNotEqual(flashcard.pk, pk_backup) |
135 | + self.assertEqual(flashcard.text, 'This is the new text') | |
118 | 136 | |
119 | 137 | def test_mask_field(self): |
120 | 138 | user = User.objects.get(email="none@none.com") |
flashcards/validators.py
View file @
5769450
... | ... | @@ -3,6 +3,8 @@ |
3 | 3 | |
4 | 4 | class FlashcardMask(set): |
5 | 5 | def __init__(self, iterable, *args, **kwargs): |
6 | + if iterable is None or iterable == '': | |
7 | + iterable = [] | |
6 | 8 | self._iterable_check(iterable) |
7 | 9 | iterable = map(tuple, iterable) |
8 | 10 | super(FlashcardMask, self).__init__(iterable, *args, **kwargs) |
flashcards/views.py
View file @
5769450
1 | +import django | |
2 | + | |
1 | 3 | from django.contrib import auth |
2 | -from flashcards.api import StandardResultsSetPagination | |
4 | +from django.core.cache import cache | |
5 | +from django.shortcuts import get_object_or_404 | |
6 | +from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection | |
3 | 7 | from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard |
4 | 8 | from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ |
5 | 9 | PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ |
6 | - FlashcardUpdateSerializer | |
7 | -from rest_framework.decorators import detail_route, permission_classes, api_view | |
10 | + FlashcardUpdateSerializer, DeepSectionSerializer | |
11 | +from rest_framework.decorators import detail_route, permission_classes, api_view, list_route | |
8 | 12 | from rest_framework.generics import ListAPIView, GenericAPIView |
9 | -from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin | |
13 | +from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin | |
10 | 14 | from rest_framework.permissions import IsAuthenticated |
11 | 15 | from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet |
12 | 16 | from django.core.mail import send_mail |
13 | 17 | from django.contrib.auth import authenticate |
14 | 18 | from django.contrib.auth.tokens import default_token_generator |
15 | -from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED | |
19 | +from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK | |
16 | 20 | from rest_framework.response import Response |
17 | 21 | from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied |
18 | 22 | from simple_email_confirmation import EmailAddress |
... | ... | @@ -20,7 +24,7 @@ |
20 | 24 | |
21 | 25 | class SectionViewSet(ReadOnlyModelViewSet): |
22 | 26 | queryset = Section.objects.all() |
23 | - serializer_class = SectionSerializer | |
27 | + serializer_class = DeepSectionSerializer | |
24 | 28 | pagination_class = StandardResultsSetPagination |
25 | 29 | permission_classes = [IsAuthenticated] |
26 | 30 | |
... | ... | @@ -30,8 +34,7 @@ |
30 | 34 | Gets flashcards for a section, excluding hidden cards. |
31 | 35 | Returned in strictly chronological order (material date). |
32 | 36 | """ |
33 | - flashcards = Flashcard.cards_visible_to(request.user).filter( \ | |
34 | - section=self.get_object()).all() | |
37 | + flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() | |
35 | 38 | return Response(FlashcardSerializer(flashcards, many=True).data) |
36 | 39 | |
37 | 40 | @detail_route(methods=['post']) |
38 | 41 | |
... | ... | @@ -40,18 +43,10 @@ |
40 | 43 | Add the current user to a specified section |
41 | 44 | If the class has a whitelist, but the user is not on the whitelist, the request will fail. |
42 | 45 | --- |
43 | - omit_serializer: true | |
44 | - parameters: | |
45 | - - fake: None | |
46 | - parameters_strategy: | |
47 | - form: replace | |
46 | + view_mocker: flashcards.api.mock_no_params | |
48 | 47 | """ |
49 | - section = self.get_object() | |
50 | - if request.user.sections.filter(pk=section.pk).exists(): | |
51 | - raise ValidationError("You are already in this section.") | |
52 | - if section.is_whitelisted and not section.is_user_on_whitelist(request.user): | |
53 | - raise PermissionDenied("You must be on the whitelist to add this section.") | |
54 | - request.user.sections.add(section) | |
48 | + | |
49 | + self.get_object().enroll(request.user) | |
55 | 50 | return Response(status=HTTP_204_NO_CONTENT) |
56 | 51 | |
57 | 52 | @detail_route(methods=['post']) |
58 | 53 | |
59 | 54 | |
60 | 55 | |
61 | 56 | |
... | ... | @@ -60,28 +55,33 @@ |
60 | 55 | Remove the current user from a specified section |
61 | 56 | If the user is not in the class, the request will fail. |
62 | 57 | --- |
63 | - omit_serializer: true | |
64 | - parameters: | |
65 | - - fake: None | |
66 | - parameters_strategy: | |
67 | - form: replace | |
58 | + view_mocker: flashcards.api.mock_no_params | |
68 | 59 | """ |
69 | - section = self.get_object() | |
70 | - if not section.user_set.filter(pk=request.user.pk).exists(): | |
71 | - raise ValidationError("You are not in the section.") | |
72 | - section.user_set.remove(request.user) | |
60 | + try: | |
61 | + self.get_object().drop(request.user) | |
62 | + except django.core.exceptions.PermissionDenied as e: | |
63 | + raise PermissionDenied(e) | |
64 | + except django.core.exceptions.ValidationError as e: | |
65 | + raise ValidationError(e) | |
73 | 66 | return Response(status=HTTP_204_NO_CONTENT) |
74 | 67 | |
75 | - @detail_route(methods=['GET']) | |
68 | + @list_route(methods=['GET']) | |
76 | 69 | def search(self, request): |
77 | 70 | """ |
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 | """ |
80 | 80 | query = request.GET.get('q', None) |
81 | 81 | if not query: return Response('[]') |
82 | 82 | qs = Section.search(query.split(' '))[:20] |
83 | - serializer = SectionSerializer(qs, many=True) | |
84 | - return Response(serializer.data) | |
83 | + data = SectionSerializer(qs, many=True).data | |
84 | + return Response(data) | |
85 | 85 | |
86 | 86 | @detail_route(methods=['GET']) |
87 | 87 | def deck(self, request, pk): |
... | ... | @@ -113,7 +113,7 @@ |
113 | 113 | |
114 | 114 | |
115 | 115 | class UserSectionListView(ListAPIView): |
116 | - serializer_class = SectionSerializer | |
116 | + serializer_class = DeepSectionSerializer | |
117 | 117 | permission_classes = [IsAuthenticated] |
118 | 118 | |
119 | 119 | def get_queryset(self): |
... | ... | @@ -126,9 +126,6 @@ |
126 | 126 | serializer_class = UserSerializer |
127 | 127 | permission_classes = [IsAuthenticated] |
128 | 128 | |
129 | - def get_queryset(self): | |
130 | - return User.objects.all() | |
131 | - | |
132 | 129 | def patch(self, request, format=None): |
133 | 130 | """ |
134 | 131 | Updates the user's password, or verifies their email address |
... | ... | @@ -188,18 +185,6 @@ |
188 | 185 | user = authenticate(**data.validated_data) |
189 | 186 | auth.login(request, user) |
190 | 187 | |
191 | - body = ''' | |
192 | - Visit the following link to confirm your email address: | |
193 | - https://flashy.cards/app/verifyemail/%s | |
194 | - | |
195 | - If you did not register for Flashy, no action is required. | |
196 | - ''' | |
197 | - | |
198 | - assert send_mail("Flashy email verification", | |
199 | - body % user.confirmation_key, | |
200 | - "noreply@flashy.cards", | |
201 | - [user.email]) | |
202 | - | |
203 | 188 | return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) |
204 | 189 | |
205 | 190 | |
... | ... | @@ -243,21 +228,7 @@ |
243 | 228 | """ |
244 | 229 | data = PasswordResetRequestSerializer(data=request.data) |
245 | 230 | data.is_valid(raise_exception=True) |
246 | - user = User.objects.get(email=data['email'].value) | |
247 | - token = default_token_generator.make_token(user) | |
248 | - | |
249 | - body = ''' | |
250 | - Visit the following link to reset your password: | |
251 | - https://flashy.cards/app/resetpassword/%d/%s | |
252 | - | |
253 | - If you did not request a password reset, no action is required. | |
254 | - ''' | |
255 | - | |
256 | - send_mail("Flashy password reset", | |
257 | - body % (user.pk, token), | |
258 | - "noreply@flashy.cards", | |
259 | - [user.email]) | |
260 | - | |
231 | + get_object_or_404(User, email=data['email'].value).request_password_reset() | |
261 | 232 | return Response(status=HTTP_204_NO_CONTENT) |
262 | 233 | |
263 | 234 | |
264 | 235 | |
265 | 236 | |
266 | 237 | |
267 | 238 | |
268 | 239 | |
269 | 240 | |
270 | 241 | |
271 | 242 | |
272 | 243 | |
273 | 244 | |
274 | 245 | |
275 | 246 | |
276 | 247 | |
277 | 248 | |
278 | 249 | |
279 | 250 | |
280 | 251 | |
... | ... | @@ -282,84 +253,83 @@ |
282 | 253 | return Response(status=HTTP_204_NO_CONTENT) |
283 | 254 | |
284 | 255 | |
285 | -class FlashcardViewSet(GenericViewSet, UpdateModelMixin, CreateModelMixin, RetrieveModelMixin): | |
256 | +class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): | |
286 | 257 | queryset = Flashcard.objects.all() |
287 | 258 | serializer_class = FlashcardSerializer |
288 | - permission_classes = [IsAuthenticated] | |
259 | + permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] | |
289 | 260 | |
290 | 261 | # Override create in CreateModelMixin |
291 | 262 | def create(self, request, *args, **kwargs): |
292 | - serializer = self.get_serializer(data=request.data) | |
263 | + serializer = FlashcardSerializer(data=request.data) | |
293 | 264 | serializer.is_valid(raise_exception=True) |
294 | - serializer.validated_data['author'] = request.user | |
295 | - self.perform_create(serializer) | |
296 | - headers = self.get_success_headers(serializer.data) | |
297 | - return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) | |
265 | + data = serializer.validated_data | |
266 | + if not request.user.is_in_section(data['section']): | |
267 | + raise PermissionDenied('The user is not enrolled in that section') | |
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 | + | |
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']) | |
300 | 288 | def report(self, request, pk): |
301 | 289 | """ |
302 | - Report the given card | |
290 | + Hide the given card | |
303 | 291 | --- |
304 | - omit_serializer: true | |
305 | - parameters: | |
306 | - - fake: None | |
307 | - parameters_strategy: | |
308 | - form: replace | |
292 | + view_mocker: flashcards.api.mock_no_params | |
309 | 293 | """ |
310 | - obj, created = FlashcardHide.objects.get_or_create(user=request.user, flashcard=self.get_object()) | |
311 | - obj.reason = request.data['reason'] | |
312 | - if created: | |
313 | - obj.save() | |
294 | + self.get_object().report(request.user) | |
314 | 295 | return Response(status=HTTP_204_NO_CONTENT) |
315 | 296 | |
297 | + hide = report | |
298 | + | |
316 | 299 | @detail_route(methods=['POST']) |
317 | 300 | def pull(self, request, pk): |
318 | 301 | """ |
319 | 302 | Pull a card from the live feed into the user's deck. |
320 | - :param request: The request object | |
321 | - :param pk: The primary key | |
322 | - :return: A 204 response upon success. | |
303 | + --- | |
304 | + view_mocker: flashcards.api.mock_no_params | |
323 | 305 | """ |
324 | 306 | flashcard = self.get_object() |
325 | 307 | user.unpull(flashcard) |
326 | 308 | return Response(status=HTTP_204_NO_CONTENT) |
327 | 309 | |
328 | - @detail_route(methods=['POST'], permission_classes=[IsAuthenticated]) | |
310 | + @detail_route(methods=['POST']) | |
329 | 311 | def unpull(self, request, pk): |
330 | 312 | """ |
331 | - TODO: delete a flashcard from the user's deck | |
313 | + Unpull a card from the user's deck | |
314 | + --- | |
315 | + view_mocker: flashcards.api.mock_no_params | |
332 | 316 | """ |
317 | + user = request.user | |
333 | 318 | flashcard = self.get_object() |
334 | - if flashcard.userFlashcard: | |
335 | - flashcard.userFlashcard.delete() | |
336 | - else: | |
337 | - raise ValidationError('You do not have this flashcard in your deck.') | |
319 | + user.unpull(flashcard) | |
338 | 320 | return Response(status=HTTP_204_NO_CONTENT) |
339 | 321 | |
340 | - | |
341 | - @detail_route(methods=['PATCH'], permission_classes=[IsAuthenticated]) | |
342 | - def update(self, request, *args, **kwargs): | |
322 | + def partial_update(self, request, *args, **kwargs): | |
343 | 323 | """ |
344 | 324 | Edit settings related to a card for the user. |
345 | - :param request: The request object. | |
346 | - :param pk: The primary key of the flashcard. | |
347 | - :return: A 204 response upon success. | |
325 | + --- | |
326 | + request_serializer: FlashcardUpdateSerializer | |
348 | 327 | """ |
349 | 328 | user = request.user |
350 | 329 | flashcard = self.get_object() |
351 | 330 | data = FlashcardUpdateSerializer(data=request.data) |
352 | 331 | data.is_valid(raise_exception=True) |
353 | 332 | new_flashcard = data.validated_data |
354 | - | |
355 | - flashcard.edit(user, new_flashcard) | |
356 | - user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=flashcard) | |
357 | - user_card.mask = flashcard.mask | |
358 | - | |
359 | - if 'mask' in new_flashcard: | |
360 | - user_card.mask = new_flashcard['mask'] | |
361 | - if 'mask' in new_flashcard or created: | |
362 | - user_card.save() | |
363 | - | |
364 | - return Response(status=HTTP_204_NO_CONTENT) | |
333 | + new_flashcard = flashcard.edit(user, new_flashcard) | |
334 | + return Response(FlashcardSerializer(new_flashcard).data, status=HTTP_200_OK) |
flashy/settings.py
View file @
5769450
... | ... | @@ -132,6 +132,12 @@ |
132 | 132 | |
133 | 133 | SECRET_KEY = os.environ.get('SECRET_KEY', 'LOL DEFAULT SECRET KEY') |
134 | 134 | |
135 | +CACHES = { | |
136 | + 'default': { | |
137 | + 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache' | |
138 | + } | |
139 | +} | |
140 | + | |
135 | 141 | SWAGGER_SETTINGS = { |
136 | 142 | 'doc_expansion': 'list' |
137 | 143 | } |
flashy/urls.py
View file @
5769450
... | ... | @@ -13,13 +13,13 @@ |
13 | 13 | |
14 | 14 | urlpatterns = [ |
15 | 15 | url(r'^api/docs/', include('rest_framework_swagger.urls')), |
16 | - url(r'^api/me$', UserDetail.as_view()), | |
17 | - url(r'^api/register', register), | |
18 | - url(r'^api/login$', login), | |
19 | - url(r'^api/logout$', logout), | |
20 | - url(r'^api/me/sections', UserSectionListView.as_view()), | |
21 | - url(r'^api/request_password_reset', request_password_reset), | |
22 | - url(r'^api/reset_password', reset_password), | |
16 | + url(r'^api/me/$', UserDetail.as_view()), | |
17 | + url(r'^api/register/', register), | |
18 | + url(r'^api/login/$', login), | |
19 | + url(r'^api/logout/$', logout), | |
20 | + url(r'^api/me/sections/', UserSectionListView.as_view()), | |
21 | + url(r'^api/request_password_reset/', request_password_reset), | |
22 | + url(r'^api/reset_password/', reset_password), | |
23 | 23 | url(r'^api/', include(router.urls)), |
24 | 24 | url(r'^admin/doc/', include('django.contrib.admindocs.urls')), |
25 | 25 | url(r'^admin/', include(admin.site.urls)), |