Commit bca16d61f71e76da820257ea976bee1bc4cf321f

Authored by Rohan Rangray
1 parent 94b93b5796
Exists in master

Added the patch method to FlashcardViewSet to edit cards.

Showing 5 changed files with 95 additions and 7 deletions Inline Diff

flashcards/migrations/0011_auto_20150514_0207.py View file @ bca16d6
File was created 1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5 import flashcards.fields
6
7
8 class Migration(migrations.Migration):
9
10 dependencies = [
11 ('flashcards', '0010_auto_20150513_1546'),
12 ]
13
14 operations = [
15 migrations.AlterModelOptions(
16 name='lectureperiod',
17 options={'ordering': ['section', 'week_day']},
18 ),
19 migrations.AlterField(
20 model_name='userflashcard',
21 name='mask',
22 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),
23 ),
24 migrations.AlterField(
25 model_name='userflashcard',
26 name='pulled',
27 field=models.DateTimeField(default=None, help_text=b'When the user pulled the card', null=True, blank=True),
28 ),
29 migrations.AlterField(
flashcards/models.py View file @ bca16d6
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.db.models import * 2 2 from django.db.models import *
from django.utils.timezone import now 3 3 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 4 4 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 5 5 from fields import MaskField
6 6
# Hack to fix AbstractUser before subclassing it 7 7 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 8 8 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 9 9 AbstractUser._meta.get_field('username')._unique = False
10 10
11 11
class EmailOnlyUserManager(UserManager): 12 12 class EmailOnlyUserManager(UserManager):
""" 13 13 """
A tiny extension of Django's UserManager which correctly creates users 14 14 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 15 15 without usernames (using emails instead).
""" 16 16 """
17 17
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 18 18 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 19 19 """
Creates and saves a User with the given email and password. 20 20 Creates and saves a User with the given email and password.
""" 21 21 """
email = self.normalize_email(email) 22 22 email = self.normalize_email(email)
user = self.model(email=email, 23 23 user = self.model(email=email,
is_staff=is_staff, is_active=True, 24 24 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 25 25 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 26 26 date_joined=now(), **extra_fields)
user.set_password(password) 27 27 user.set_password(password)
user.save(using=self._db) 28 28 user.save(using=self._db)
return user 29 29 return user
30 30
def create_user(self, email, password=None, **extra_fields): 31 31 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 32 32 return self._create_user(email, password, False, False, **extra_fields)
33 33
def create_superuser(self, email, password, **extra_fields): 34 34 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 35 35 return self._create_user(email, password, True, True, **extra_fields)
36 36
37 37
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 38 38 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 39 39 """
An extension of Django's default user model. 40 40 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 41 41 We use email as the username field, and include enrolled sections here
""" 42 42 """
objects = EmailOnlyUserManager() 43 43 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 44 44 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 45 45 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 46 46 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
47 47
48 48
class UserFlashcard(Model): 49 49 class UserFlashcard(Model):
""" 50 50 """
Represents the relationship between a user and a flashcard by: 51 51 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 52 52 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 53 53 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 54 54 3. A user has a flashcard hidden from them
""" 55 55 """
user = ForeignKey('User') 56 56 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card") 57 57 mask = MaskField(max_length=255, null=True, blank=True, default=None, help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") 58 58 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 59 59 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 60 60 unpulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user unpulled this card")
61 61
class Meta: 62 62 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 63 63 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 64 64 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 65 65 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 66 66 # By default, order by most recently pulled
ordering = ['-pulled'] 67 67 ordering = ['-pulled']
68 68
def is_hidden(self): 69 69 def is_hidden(self):
""" 70 70 """
A card is hidden only if a user has not ever added it to their deck. 71 71 A card is hidden only if a user has not ever added it to their deck.
:return: Whether the flashcard is hidden from the user 72 72 :return: Whether the flashcard is hidden from the user
""" 73 73 """
return not self.pulled 74 74 return not self.pulled
75 75
def is_in_deck(self): 76 76 def is_in_deck(self):
""" 77 77 """
:return:Whether the flashcard is in the user's deck 78 78 :return:Whether the flashcard is in the user's deck
""" 79 79 """
return self.pulled and not self.unpulled 80 80 return self.pulled and not self.unpulled
81 81
82 82
class Flashcard(Model): 83 83 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 84 84 text = CharField(max_length=255, help_text='The text on the card')
section = ForeignKey('Section', help_text='The section with which the card is associated') 85 85 section = ForeignKey('Section', help_text='The section with which the card is associated')
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") 86 86 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(default=now, help_text="The date with which the card is associated") 87 87 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 88 88 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 89 89 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 90 90 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 91 91 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 92 92 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 93 93 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
94 94
class Meta: 95 95 class Meta:
# By default, order by most recently pushed 96 96 # By default, order by most recently pushed
ordering = ['-pushed'] 97 97 ordering = ['-pushed']
98 98
def is_hidden_from(self, user): 99 99 def is_hidden_from(self, user):
""" 100 100 """
A card can be hidden globally, but if a user has the card in their deck, 101 101 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 102 102 this visibility overrides a global hide.
:param user: 103 103 :param user:
:return: Whether the card is hidden from the user. 104 104 :return: Whether the card is hidden from the user.
""" 105 105 """
result = user.userflashcard_set.filter(flashcard=self) 106 106 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 107 107 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 108 108 return result[0].is_hidden()
109 109
@classmethod 110 110 @classmethod
def cards_visible_to(cls, user): 111 111 def cards_visible_to(cls, user):
""" 112 112 """
:param user: 113 113 :param user:
:return: A queryset with all cards that should be visible to a user. 114 114 :return: A queryset with all cards that should be visible to a user.
""" 115 115 """
return cls.objects.filter(is_hidden=False).exclude(userflashcard__user=user, userflashcard__pulled=None) 116 116 return cls.objects.filter(is_hidden=False).exclude(userflashcard__user=user, userflashcard__pulled=None)
117 117
118 118
class UserFlashcardQuiz(Model): 119 119 class UserFlashcardQuiz(Model):
""" 120 120 """
An event of a user being quizzed on a flashcard. 121 121 An event of a user being quizzed on a flashcard.
""" 122 122 """
user_flashcard = ForeignKey(UserFlashcard) 123 123 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 124 124 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 125 125 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") 126 126 response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 127 127 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
128 128
def status(self): 129 129 def status(self):
""" 130 130 """
There are three stages of a quiz object: 131 131 There are three stages of a quiz object:
1. the user has been shown the card 132 132 1. the user has been shown the card
2. the user has answered the card 133 133 2. the user has answered the card
3. the user has self-evaluated their response's correctness 134 134 3. the user has self-evaluated their response's correctness
135 135
:return: string (evaluated, answered, viewed) 136 136 :return: string (evaluated, answered, viewed)
""" 137 137 """
if self.correct is not None: return "evaluated" 138 138 if self.correct is not None: return "evaluated"
if self.response: return "answered" 139 139 if self.response: return "answered"
return "viewed" 140 140 return "viewed"
141 141
142 142
class Section(Model): 143 143 class Section(Model):
""" 144 144 """
A UCSD course taught by an instructor during a quarter. 145 145 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 146 146 We use the term "section" to avoid collision with the builtin keyword "class"
We index gratuitously to support autofill and because this is primarily read-only 147 147 We index gratuitously to support autofill and because this is primarily read-only
""" 148 148 """
department = CharField(db_index=True, max_length=50) 149 149 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 150 150 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 151 151 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 152 152 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 153 153 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 154 154 quarter = CharField(db_index=True, max_length=4)
155 155
@classmethod 156 156 @classmethod
def search(cls, terms): 157 157 def search(cls, terms):
""" 158 158 """
Search all fields of all sections for a particular set of terms 159 159 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 160 160 A matching section must match at least one field on each term
:param terms:iterable 161 161 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 162 162 :return: Matching QuerySet ordered by department and course number
""" 163 163 """
final_q = Q() 164 164 final_q = Q()
for term in terms: 165 165 for term in terms:
q = Q(department__icontains=term) 166 166 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 167 167 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 168 168 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 169 169 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 170 170 q |= Q(instructor__icontains=term)
final_q &= q 171 171 final_q &= q
qs = cls.objects.filter(final_q) 172 172 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 173 173 # Have the database cast the course number to an integer so it will sort properly
# ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically 174 174 # ECE 35 should rank before ECE 135 even though '135' < '35' lexicographically
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) 175 175 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 176 176 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 177 177 return qs
178 178
@property 179 179 @property
def is_whitelisted(self): 180 180 def is_whitelisted(self):
""" 181 181 """
:return: whether a whitelist exists for this section 182 182 :return: whether a whitelist exists for this section
""" 183 183 """
return self.whitelist.exists() 184 184 return self.whitelist.exists()
185 185
def is_user_on_whitelist(self, user): 186 186 def is_user_on_whitelist(self, user):
""" 187 187 """
:return: whether the user is on the waitlist for this section 188 188 :return: whether the user is on the waitlist for this section
""" 189 189 """
return self.whitelist.filter(email=user.email).exists() 190 190 return self.whitelist.filter(email=user.email).exists()
191 191
class Meta: 192 192 class Meta:
ordering = ['-course_title'] 193 193 ordering = ['-course_title']
194 194
@property 195 195 @property
def lecture_times(self): 196 196 def lecture_times(self):
return ', '.join(map(lambda x: '%s %s' % (x.weekday_letter, x.short_start_time), self.lectureperiod_set.all())) 197 197 return ', '.join(map(lambda x: '%s %s' % (x.weekday_letter, x.short_start_time), self.lectureperiod_set.all()))
198 198
@property 199 199 @property
def long_name(self): 200 200 def long_name(self):
return '%s (%s) (%s)' % (self.course_title, self.instructor, self.lecture_times) 201 201 return '%s (%s) (%s)' % (self.course_title, self.instructor, self.lecture_times)
202 202
@property 203 203 @property
def short_name(self): 204 204 def short_name(self):
return '%s %s' % (self.department_abbreviation, self.course_num) 205 205 return '%s %s' % (self.department_abbreviation, self.course_num)
206 206
def __unicode__(self): 207 207 def __unicode__(self):
return '%s %s: %s (%s %s)' % ( 208 208 return '%s %s: %s (%s %s)' % (
self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) 209 209 self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter)
210 210
211 211
class LecturePeriod(Model): 212 212 class LecturePeriod(Model):
flashcards/serializers.py View file @ bca16d6
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 2 2 from django.utils.timezone import now
import pytz 3 3 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard 4 4 from flashcards.models import Section, LecturePeriod, User, Flashcard
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 6 6 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 7 7 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField
from rest_framework.relations import HyperlinkedRelatedField 8 8 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 9 9 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 10 10 from rest_framework.validators import UniqueValidator
from json import dumps, loads 11 11 from json import dumps, loads
12 12
13 13
class EmailSerializer(Serializer): 14 14 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 15 email = EmailField(required=True)
16 16
17 17
class EmailPasswordSerializer(EmailSerializer): 18 18 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 19 password = CharField(required=True)
20 20
21 21
class RegistrationSerializer(EmailPasswordSerializer): 22 22 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 23 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 24
25 25
class PasswordResetRequestSerializer(EmailSerializer): 26 26 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 27 def validate_email(self, value):
try: 28 28 try:
User.objects.get(email=value) 29 29 User.objects.get(email=value)
return value 30 30 return value
except User.DoesNotExist: 31 31 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 32 raise serializers.ValidationError('No user exists with that email')
33 33
34 34
class PasswordResetSerializer(Serializer): 35 35 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 36 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 37 uid = IntegerField(required=True)
token = CharField(required=True) 38 38 token = CharField(required=True)
39 39
def validate_uid(self, value): 40 40 def validate_uid(self, value):
try: 41 41 try:
User.objects.get(id=value) 42 42 User.objects.get(id=value)
return value 43 43 return value
except User.DoesNotExist: 44 44 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 45 raise serializers.ValidationError('Could not verify reset token')
46 46
47 47
class UserUpdateSerializer(Serializer): 48 48 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 49 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 50 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 51 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 52 # reset_token = CharField(required=False)
53 53
def validate(self, data): 54 54 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 55 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 56 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 57 return data
58 58
59 59
class Password(Serializer): 60 60 class Password(Serializer):
email = EmailField(required=True) 61 61 email = EmailField(required=True)
password = CharField(required=True) 62 62 password = CharField(required=True)
63 63
64 64
class LecturePeriodSerializer(ModelSerializer): 65 65 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 66 class Meta:
model = LecturePeriod 67 67 model = LecturePeriod
exclude = 'id', 'section' 68 68 exclude = 'id', 'section'
69 69
70 70
class SectionSerializer(ModelSerializer): 71 71 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 72 72 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
short_name = CharField() 73 73 short_name = CharField()
long_name = CharField() 74 74 long_name = CharField()
75 75
class Meta: 76 76 class Meta:
model = Section 77 77 model = Section
78 78
79 79
class UserSerializer(ModelSerializer): 80 80 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 81 81 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 82 82 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 83 83 is_confirmed = BooleanField()
84 84
class Meta: 85 85 class Meta:
model = User 86 86 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 87 87 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
88 88
89 89
class MaskFieldSerializer(serializers.Field): 90 90 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 91 91 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 92 92 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 93 93 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 94 94 'overlap': 'Ensure this field does not have overlapping intervals.'
} 95 95 }
96 96
def to_representation(self, value): 97 97 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 98 98 return dumps(list(self._make_mask(value)))
99 99
def to_internal_value(self, value): 100 100 def to_internal_value(self, value):
return self._make_mask(loads(value)) 101 101 return self._make_mask(loads(value))
102 102
def _make_mask(self, data): 103 103 def _make_mask(self, data):
try: 104 104 try:
mask = FlashcardMask(data) 105 105 mask = FlashcardMask(data)
except ValueError: 106 106 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 107 107 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 108 108 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 109 109 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 110 110 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 111 111 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 112 112 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 113 113 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 114 114 return mask
115 115
116 116
class FlashcardSerializer(ModelSerializer): 117 117 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 118 118 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 119 119 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 120 120 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer() 121 121 mask = MaskFieldSerializer()
122 122
def validate_material_date(self, value): 123 123 def validate_material_date(self, value):
utc = pytz.UTC 124 124 utc = pytz.UTC
# TODO: make this dynamic 125 125 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 126 126 quarter_start = utc.localize(datetime(2015, 3, 15))
quarter_end = utc.localize(datetime(2015, 6, 15)) 127 127 quarter_end = utc.localize(datetime(2015, 6, 15))
128 128
if quarter_start <= value <= quarter_end: 129 129 if quarter_start <= value <= quarter_end:
return value 130 130 return value
else: 131 131 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 132 132 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
133 133
def validate_previous(self, value): 134 134 def validate_previous(self, value):
if value is None: 135 135 if value is None:
return value 136 136 return value
if Flashcard.objects.filter(pk=value).count() > 0: 137 137 if Flashcard.objects.filter(pk=value).count() > 0:
return value 138 138 return value
flashcards/validators.py View file @ bca16d6
__author__ = 'rray' 1
2
from collections import Iterable 3 1 from collections import Iterable
4 2
5 3
class FlashcardMask(set): 6 4 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 7 5 def __init__(self, iterable, *args, **kwargs):
iterable = map(tuple, iterable) 8 6 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 9 7 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._iterable_check() 10 8 self._iterable_check()
self._interval_check() 11 9 self._interval_check()
self._overlap_check() 12 10 self._overlap_check()
13 11
12 def max_offset(self):
13 return self._end
14
def _iterable_check(self): 14 15 def _iterable_check(self):
if not all([isinstance(i, Iterable) for i in self]): 15 16 if not all([isinstance(i, Iterable) for i in self]):
raise TypeError("Interval not a valid iterable") 16 17 raise TypeError("Interval not a valid iterable")
17 18
def _interval_check(self): 18 19 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 19 20 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 20 21 raise TypeError("Intervals must have exactly 2 elements, begin and end")
21 22
def _overlap_check(self): 22 23 def _overlap_check(self):
p_beg, p_end = -1, -1 23 24 p_beg, p_end = -1, -1
for interval in sorted(self): 24 25 for interval in sorted(self):
beg, end = map(int, interval) 25 26 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 26 27 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask") 27 28 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 28 29 p_beg, p_end = beg, end
30 self._end = end
flashcards/views.py View file @ bca16d6
from django.contrib import auth 1 1 from django.contrib import auth
from django.db.models import Q 2 2 from django.db.models import Q
from flashcards.api import StandardResultsSetPagination 3 3 from flashcards.api import StandardResultsSetPagination
from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard 4 4 from flashcards.models import Section, User, Flashcard, FlashcardReport, UserFlashcard
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 5 5 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer 6 6 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
7 FlashcardUpdateSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 7 8 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 8 9 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 9 10 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 10 11 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 11 12 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 12 13 from django.core.mail import send_mail
from django.contrib.auth import authenticate 13 14 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 14 15 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 15 16 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 16 17 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 17 18 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 18 19 from simple_email_confirmation import EmailAddress
20 from datetime import datetime
19 21
20 22
class SectionViewSet(ReadOnlyModelViewSet): 21 23 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 22 24 queryset = Section.objects.all()
serializer_class = SectionSerializer 23 25 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 24 26 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 25 27 permission_classes = [IsAuthenticated]
26 28
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 27 29 @detail_route(methods=['get'], permission_classes=[IsAuthenticated])
def flashcards(self, request, pk): 28 30 def flashcards(self, request, pk):
""" 29 31 """
Gets flashcards for a section, excluding hidden cards. 30 32 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 31 33 Returned in strictly chronological order (material date).
""" 32 34 """
flashcards = Flashcard.cards_visible_to(request.user).filter( \ 33 35 flashcards = Flashcard.cards_visible_to(request.user).filter( \
section=self.get_object(), is_hidden=False).all() 34 36 section=self.get_object(), is_hidden=False).all()
35 37
return Response(FlashcardSerializer(flashcards, many=True).data) 36 38 return Response(FlashcardSerializer(flashcards, many=True).data)
37 39
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 38 40 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 39 41 def enroll(self, request, pk):
""" 40 42 """
Add the current user to a specified section 41 43 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 42 44 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 43 45 ---
omit_serializer: true 44 46 omit_serializer: true
parameters: 45 47 parameters:
- fake: None 46 48 - fake: None
parameters_strategy: 47 49 parameters_strategy:
form: replace 48 50 form: replace
""" 49 51 """
section = self.get_object() 50 52 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 51 53 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 52 54 raise ValidationError("You are already in this section.")
if section.is_whitelisted and not section.is_user_on_whitelist(request.user): 53 55 if section.is_whitelisted and not section.is_user_on_whitelist(request.user):
raise PermissionDenied("You must be on the whitelist to add this section.") 54 56 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 55 57 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 56 58 return Response(status=HTTP_204_NO_CONTENT)
57 59
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 58 60 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 59 61 def drop(self, request, pk):
""" 60 62 """
Remove the current user from a specified section 61 63 Remove the current user from a specified section
If the user is not in the class, the request will fail. 62 64 If the user is not in the class, the request will fail.
--- 63 65 ---
omit_serializer: true 64 66 omit_serializer: true
parameters: 65 67 parameters:
- fake: None 66 68 - fake: None
parameters_strategy: 67 69 parameters_strategy:
form: replace 68 70 form: replace
""" 69 71 """
section = self.get_object() 70 72 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 71 73 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 72 74 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 73 75 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 74 76 return Response(status=HTTP_204_NO_CONTENT)
75 77
@list_route(methods=['get'], permission_classes=[IsAuthenticated]) 76 78 @list_route(methods=['get'], permission_classes=[IsAuthenticated])
def search(self, request): 77 79 def search(self, request):
query = request.GET.get('q',None) 78 80 query = request.GET.get('q',None)
if not query: return Response('[]') 79 81 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:8] 80 82 qs = Section.search(query.split(' '))[:8]
serializer = SectionSerializer(qs, many=True) 81 83 serializer = SectionSerializer(qs, many=True)
return Response(serializer.data) 82 84 return Response(serializer.data)
83 85
84 86
class UserSectionListView(ListAPIView): 85 87 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 86 88 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 87 89 permission_classes = [IsAuthenticated]
88 90
def get_queryset(self): 89 91 def get_queryset(self):
return self.request.user.sections.all() 90 92 return self.request.user.sections.all()
91 93
def paginate_queryset(self, queryset): return None 92 94 def paginate_queryset(self, queryset): return None
93 95
94 96
class UserDetail(GenericAPIView): 95 97 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 96 98 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 97 99 permission_classes = [IsAuthenticated]
98 100
def get_queryset(self): 99 101 def get_queryset(self):
return User.objects.all() 100 102 return User.objects.all()
101 103
def patch(self, request, format=None): 102 104 def patch(self, request, format=None):
""" 103 105 """
Updates the user's password, or verifies their email address 104 106 Updates the user's password, or verifies their email address
--- 105 107 ---
request_serializer: UserUpdateSerializer 106 108 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 107 109 response_serializer: UserSerializer
""" 108 110 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 109 111 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 110 112 data.is_valid(raise_exception=True)
data = data.validated_data 111 113 data = data.validated_data
112 114
if 'new_password' in data: 113 115 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 114 116 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 115 117 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 116 118 request.user.set_password(data['new_password'])
request.user.save() 117 119 request.user.save()
118 120
if 'confirmation_key' in data: 119 121 if 'confirmation_key' in data:
try: 120 122 try:
request.user.confirm_email(data['confirmation_key']) 121 123 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 122 124 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 123 125 raise ValidationError('confirmation_key is invalid')
124 126
return Response(UserSerializer(request.user).data) 125 127 return Response(UserSerializer(request.user).data)
126 128
def get(self, request, format=None): 127 129 def get(self, request, format=None):
""" 128 130 """
Return data about the user 129 131 Return data about the user
--- 130 132 ---
response_serializer: UserSerializer 131 133 response_serializer: UserSerializer
""" 132 134 """
serializer = UserSerializer(request.user, context={'request': request}) 133 135 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 134 136 return Response(serializer.data)
135 137
def delete(self, request): 136 138 def delete(self, request):
""" 137 139 """
Irrevocably delete the user and their data 138 140 Irrevocably delete the user and their data
139 141
Yes, really 140 142 Yes, really
""" 141 143 """
request.user.delete() 142 144 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 143 145 return Response(status=HTTP_204_NO_CONTENT)
144 146
145 147
@api_view(['POST']) 146 148 @api_view(['POST'])
def register(request, format=None): 147 149 def register(request, format=None):
""" 148 150 """
Register a new user 149 151 Register a new user
--- 150 152 ---
request_serializer: EmailPasswordSerializer 151 153 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 152 154 response_serializer: UserSerializer
""" 153 155 """
data = RegistrationSerializer(data=request.data) 154 156 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 155 157 data.is_valid(raise_exception=True)
156 158
User.objects.create_user(**data.validated_data) 157 159 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 158 160 user = authenticate(**data.validated_data)
auth.login(request, user) 159 161 auth.login(request, user)
160 162
body = ''' 161 163 body = '''
Visit the following link to confirm your email address: 162 164 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 163 165 https://flashy.cards/app/verify_email/%s
164 166
If you did not register for Flashy, no action is required. 165 167 If you did not register for Flashy, no action is required.
''' 166 168 '''
167 169
assert send_mail("Flashy email verification", 168 170 assert send_mail("Flashy email verification",
body % user.confirmation_key, 169 171 body % user.confirmation_key,
"noreply@flashy.cards", 170 172 "noreply@flashy.cards",
[user.email]) 171 173 [user.email])
172 174
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 173 175 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
174 176
175 177
@api_view(['POST']) 176 178 @api_view(['POST'])
def login(request): 177 179 def login(request):
""" 178 180 """
Authenticates user and returns user data if valid. 179 181 Authenticates user and returns user data if valid.
--- 180 182 ---
request_serializer: EmailPasswordSerializer 181 183 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 182 184 response_serializer: UserSerializer
""" 183 185 """
184 186
data = EmailPasswordSerializer(data=request.data) 185 187 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 186 188 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 187 189 user = authenticate(**data.validated_data)
188 190
if user is None: 189 191 if user is None:
raise AuthenticationFailed('Invalid email or password') 190 192 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 191 193 if not user.is_active:
raise NotAuthenticated('Account is disabled') 192 194 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 193 195 auth.login(request, user)
return Response(UserSerializer(request.user).data) 194 196 return Response(UserSerializer(request.user).data)
195 197
196 198
@api_view(['POST']) 197 199 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 198 200 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 199 201 def logout(request, format=None):
""" 200 202 """
Logs the authenticated user out. 201 203 Logs the authenticated user out.
""" 202 204 """
auth.logout(request) 203 205 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 204 206 return Response(status=HTTP_204_NO_CONTENT)
205 207
206 208
@api_view(['POST']) 207 209 @api_view(['POST'])
def request_password_reset(request, format=None): 208 210 def request_password_reset(request, format=None):
""" 209 211 """
Send a password reset token/link to the provided email. 210 212 Send a password reset token/link to the provided email.
--- 211 213 ---
request_serializer: PasswordResetRequestSerializer 212 214 request_serializer: PasswordResetRequestSerializer
""" 213 215 """
data = PasswordResetRequestSerializer(data=request.data) 214 216 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 215 217 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 216 218 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 217 219 token = default_token_generator.make_token(user)
218 220
body = ''' 219 221 body = '''
Visit the following link to reset your password: 220 222 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 221 223 https://flashy.cards/app/reset_password/%d/%s
222 224
If you did not request a password reset, no action is required. 223 225 If you did not request a password reset, no action is required.
''' 224 226 '''
225 227
send_mail("Flashy password reset", 226 228 send_mail("Flashy password reset",
body % (user.pk, token), 227 229 body % (user.pk, token),
"noreply@flashy.cards", 228 230 "noreply@flashy.cards",
[user.email]) 229 231 [user.email])
230 232
return Response(status=HTTP_204_NO_CONTENT) 231 233 return Response(status=HTTP_204_NO_CONTENT)
232 234
233 235
@api_view(['POST']) 234 236 @api_view(['POST'])
def reset_password(request, format=None): 235 237 def reset_password(request, format=None):
""" 236 238 """
Updates user's password to new password if token is valid. 237 239 Updates user's password to new password if token is valid.
--- 238 240 ---
request_serializer: PasswordResetSerializer 239 241 request_serializer: PasswordResetSerializer
""" 240 242 """
data = PasswordResetSerializer(data=request.data) 241 243 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 242 244 data.is_valid(raise_exception=True)
243 245
user = User.objects.get(id=data['uid'].value) 244 246 user = User.objects.get(id=data['uid'].value)
# Check token validity. 245 247 # Check token validity.
246 248
if default_token_generator.check_token(user, data['token'].value): 247 249 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 248 250 user.set_password(data['new_password'].value)
user.save() 249 251 user.save()
else: 250 252 else:
raise ValidationError('Could not verify reset token') 251 253 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 252 254 return Response(status=HTTP_204_NO_CONTENT)
253 255
254 256
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 255 257 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 256 258 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 257 259 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated] 258 260 permission_classes = [IsAuthenticated]
259 261
# Override create in CreateModelMixin 260 262 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 261 263 def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data) 262 264 serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True) 263 265 serializer.is_valid(raise_exception=True)
serializer.validated_data['author'] = request.user 264 266 serializer.validated_data['author'] = request.user
self.perform_create(serializer) 265 267 self.perform_create(serializer)
headers = self.get_success_headers(serializer.data) 266 268 headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=HTTP_201_CREATED, headers=headers) 267 269 return Response(serializer.data, status=HTTP_201_CREATED, headers=headers)
268 270
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 269 271 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def report(self, request, pk): 270 272 def report(self, request, pk):
""" 271 273 """
Report the given card 272 274 Report the given card
--- 273 275 ---
omit_serializer: true 274 276 omit_serializer: true
parameters: 275 277 parameters:
- fake: None 276 278 - fake: None
parameters_strategy: 277 279 parameters_strategy:
form: replace 278 280 form: replace
""" 279 281 """
obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object()) 280 282 obj, created = FlashcardReport.objects.get_or_create(user=request.user, flashcard=self.get_object())
obj.reason = request.data['reason'] 281 283 obj.reason = request.data['reason']
obj.save() 282 284 obj.save()