Commit 5769450a64e1fb8413aca028936ff578ebe18fd4

Authored by Laura Hawkins
Exists in master

trying to pull

Showing 15 changed files Side-by-side Diff

... ... @@ -30,4 +30,9 @@
30 30 scripts/run_local.sh
31 31  
32 32 This requires that flashy-backend be in the directory as flashy-frontend.
  33 +
  34 +Import sections from JSON:
  35 +
  36 + python manage.py loaddata sections
  37 +
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)),