Commit fe0d3701626a4916f0947942424d57aaadd79ef9

Authored by Andrew Buss
1 parent 21835759b7
Exists in master

changed some things for autocomplete

Showing 3 changed files with 8 additions and 4 deletions Inline Diff

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