Commit fe6a4ff639924bc30df4bf07fbb6af1ca4bff366

Authored by Rohan Rangray
1 parent ec4e278d01
Exists in master

Replaced FlashcardMask with MaskField

Showing 6 changed files with 153 additions and 9 deletions Inline Diff

flashcards/admin.py View file @ fe6a4ff
from django.contrib import admin 1 1 from django.contrib import admin
from django.contrib.auth.admin import UserAdmin 2 2 from django.contrib.auth.admin import UserAdmin
from flashcards.models import Flashcard, UserFlashcard, Section, FlashcardMask, \ 3 3 from flashcards.models import Flashcard, UserFlashcard, Section, \
LecturePeriod, User, UserFlashcardQuiz 4 4 LecturePeriod, User, UserFlashcardQuiz
5 5
admin.site.register([ 6 6 admin.site.register([
Flashcard, 7 7 Flashcard,
FlashcardMask, 8
UserFlashcard, 9 8 UserFlashcard,
UserFlashcardQuiz, 10 9 UserFlashcardQuiz,
Section, 11 10 Section,
LecturePeriod 12 11 LecturePeriod
]) 13 12 ])
admin.site.register(User, UserAdmin) 14 13 admin.site.register(User, UserAdmin)
flashcards/fields.py View file @ fe6a4ff
File was created 1 __author__ = 'rray'
2
3 from django.db import models
4
5
6 class MaskField(models.Field):
7 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
8 self.blank_sep = blank_sep
9 self.range_sep = range_sep
10 super(MaskField, self).__init__(*args, **kwargs)
11
12 def deconstruct(self):
13 name, path, args, kwargs = super(MaskField, self).deconstruct()
14 if self.blank_sep != ',':
15 kwargs['blank_sep'] = self.blank_sep
16 if self.range_sep != '-':
17 kwargs['range_sep'] = self.range_sep
18 return name, path, args, kwargs
19
20 def db_type(self, connection):
21 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
22 return 'varchar'
23 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
24 return 'integer[2][]'
25
26 def from_db_value(self, value, expression, connection, context):
27 if value is None:
28 return value
29 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
30 return MaskField._sqlite_parse_mask(value)
31 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
32 return MaskField._psql_parse_mask(value)
33
34 def get_db_prep_value(self, value, connection, prepared=False):
35 if not prepared:
36 value = self.get_prep_value(value)
37 if value is None:
38 return value
39 if connection.settings_dict['ENGINE'] == 'django.db.backends.sqlite3':
40 return ','.join(['-'.join(map(str, i)) for i in value])
41 if connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2':
42 return value
43
44 def to_python(self, value):
45 if value is None or isinstance(value, set):
46 return value
47 return MaskField._parse_mask(value)
48
49 def get_prep_value(self, value):
50 if value is None:
51 return value
52 if not isinstance(value, set) or not all([isinstance(interval, tuple) for interval in value]):
53 raise ValueError("Invalid value for MaskField attribute")
54 return sorted([list(interval) for interval in value])
55
56 def get_prep_lookup(self, lookup_type, value):
57 raise TypeError("Lookup not supported for MaskField")
58
59 @staticmethod
60 def _parse_mask(intervals):
61 p_beg, p_end = -1, -1
62 mask_list = []
63 for interval in intervals:
64 beg, end = map(int, interval)
65 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
66 raise ValueError("Invalid range offsets in the mask")
67 mask_list.append((beg, end))
68 p_beg, p_end = beg, end
69 return set(mask_list)
70
71 @staticmethod
flashcards/models.py View file @ fe6a4ff
from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin, 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
5 from fields import MaskField
5 6
# Hack to fix AbstractUser before subclassing it 6 7 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 7 8 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 8 9 AbstractUser._meta.get_field('username')._unique = False
9 10
10 11
class EmailOnlyUserManager(UserManager): 11 12 class EmailOnlyUserManager(UserManager):
""" 12 13 """
A tiny extension of Django's UserManager which correctly creates users 13 14 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 14 15 without usernames (using emails instead).
""" 15 16 """
16 17
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 17 18 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 18 19 """
Creates and saves a User with the given email and password. 19 20 Creates and saves a User with the given email and password.
""" 20 21 """
email = self.normalize_email(email) 21 22 email = self.normalize_email(email)
user = self.model(email=email, 22 23 user = self.model(email=email,
is_staff=is_staff, is_active=True, 23 24 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 24 25 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 25 26 date_joined=now(), **extra_fields)
user.set_password(password) 26 27 user.set_password(password)
user.save(using=self._db) 27 28 user.save(using=self._db)
return user 28 29 return user
29 30
def create_user(self, email, password=None, **extra_fields): 30 31 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 31 32 return self._create_user(email, password, False, False, **extra_fields)
32 33
def create_superuser(self, email, password, **extra_fields): 33 34 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 34 35 return self._create_user(email, password, True, True, **extra_fields)
35 36
36 37
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 37 38 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 38 39 """
An extension of Django's default user model. 39 40 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 40 41 We use email as the username field, and include enrolled sections here
""" 41 42 """
objects = EmailOnlyUserManager() 42 43 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 43 44 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 44 45 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 45 46 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
46 47
47 48
class UserFlashcard(Model): 48 49 class UserFlashcard(Model):
""" 49 50 """
Represents the relationship between a user and a flashcard by: 50 51 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 51 52 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 52 53 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 53 54 3. A user has a flashcard hidden from them
""" 54 55 """
user = ForeignKey('User') 55 56 user = ForeignKey('User')
mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask") 56 57 # mask = ForeignKey('FlashcardMask', blank=True, null=True, help_text="A mask which overrides the card's mask")
58 mask = MaskField(max_length=255, null=True, blank=True, help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card") 57 59 pulled = DateTimeField(blank=True, null=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 58 60 flashcard = ForeignKey('Flashcard')
unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card") 59 61 unpulled = DateTimeField(blank=True, null=True, help_text="When the user unpulled this card")
60 62
class Meta: 61 63 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 62 64 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 63 65 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 64 66 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 65 67 # By default, order by most recently pulled
ordering = ['-pulled'] 66 68 ordering = ['-pulled']
67 69
def is_hidden(self): 68 70 def is_hidden(self):
""" 69 71 """
A card is hidden only if a user has not ever added it to their deck. 70 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 71 73 :return: Whether the flashcard is hidden from the user
""" 72 74 """
return not self.pulled 73 75 return not self.pulled
74 76
def is_in_deck(self): 75 77 def is_in_deck(self):
""" 76 78 """
:return:Whether the flashcard is in the user's deck 77 79 :return:Whether the flashcard is in the user's deck
""" 78 80 """
return self.pulled and not self.unpulled 79 81 return self.pulled and not self.unpulled
80 82
81 83
84
85 """
class FlashcardMask(Model): 82 86 class FlashcardMask(Model):
""" 83
A serialized list of character ranges that can be blanked out during a quiz. 84 87 A serialized list of character ranges that can be blanked out during a quiz.
This is encoded as '13-145,150-195' 85 88 This is encoded as '13-145,150-195'. The ranges are 0-indexed and inclusive.
""" 86 89
ranges = CharField(max_length=255) 87 90 ranges = CharField(max_length=255)
91 """
88 92
89 93
class Flashcard(Model): 90 94 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 91 95 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') 92 96 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") 93 97 pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed")
material_date = DateTimeField(help_text="The date with which the card is associated") 94 98 material_date = DateTimeField(help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 95 99 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 96 100 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 97 101 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 98 102 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 99 103 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card") 100 104 # mask = ForeignKey(FlashcardMask, blank=True, null=True, help_text="The default mask for this card")
105 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
101 106
class Meta: 102 107 class Meta:
# By default, order by most recently pushed 103 108 # By default, order by most recently pushed
ordering = ['-pushed'] 104 109 ordering = ['-pushed']
105 110
def is_hidden_from(self, user): 106 111 def is_hidden_from(self, user):
""" 107 112 """
A card can be hidden globally, but if a user has the card in their deck, 108 113 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 109 114 this visibility overrides a global hide.
:param user: 110 115 :param user:
:return: Whether the card is hidden from the user. 111 116 :return: Whether the card is hidden from the user.
""" 112 117 """
result = user.userflashcard_set.filter(flashcard=self) 113 118 result = user.userflashcard_set.filter(flashcard=self)
if not result.exists(): return self.is_hidden 114 119 if not result.exists(): return self.is_hidden
return result[0].is_hidden() 115 120 return result[0].is_hidden()
116 121
117 122
@classmethod 118 123 @classmethod
def cards_visible_to(cls, user): 119 124 def cards_visible_to(cls, user):
""" 120 125 """
:param user: 121 126 :param user:
:return: A queryset with all cards that should be visible to a user. 122 127 :return: A queryset with all cards that should be visible to a user.
""" 123 128 """
return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None) 124 129 return cls.objects.filter(hidden=False).exclude(userflashcard=user, userflashcard__pulled=None)
125 130
126 131
class UserFlashcardQuiz(Model): 127 132 class UserFlashcardQuiz(Model):
""" 128 133 """
An event of a user being quizzed on a flashcard. 129 134 An event of a user being quizzed on a flashcard.
""" 130 135 """
user_flashcard = ForeignKey(UserFlashcard) 131 136 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField() 132 137 when = DateTimeField()
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 133 138 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") 134 139 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") 135 140 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
136 141
def status(self): 137 142 def status(self):
""" 138 143 """
There are three stages of a quiz object: 139 144 There are three stages of a quiz object:
1. the user has been shown the card 140 145 1. the user has been shown the card
2. the user has answered the card 141 146 2. the user has answered the card
3. the user has self-evaluated their response's correctness 142 147 3. the user has self-evaluated their response's correctness
143 148
:return: string (evaluated, answered, viewed) 144 149 :return: string (evaluated, answered, viewed)
""" 145 150 """
if self.correct is not None: return "evaluated" 146 151 if self.correct is not None: return "evaluated"
if self.response: return "answered" 147 152 if self.response: return "answered"
return "viewed" 148 153 return "viewed"
149 154
150 155
class Section(Model): 151 156 class Section(Model):
""" 152 157 """
A UCSD course taught by an instructor during a quarter. 153 158 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 154 159 We use the term "section" to avoid collision with the builtin keyword "class"
""" 155 160 """
department = CharField(max_length=50) 156 161 department = CharField(max_length=50)
course_num = CharField(max_length=6) 157 162 course_num = CharField(max_length=6)
course_title = CharField(max_length=50) 158 163 course_title = CharField(max_length=50)
instructor = CharField(max_length=100) 159 164 instructor = CharField(max_length=100)
quarter = CharField(max_length=4) 160 165 quarter = CharField(max_length=4)
161 166
def is_whitelisted(self): 162 167 def is_whitelisted(self):
""" 163 168 """
:return: whether a whitelist exists for this section 164 169 :return: whether a whitelist exists for this section
""" 165 170 """
return self.whitelist.exists() 166 171 return self.whitelist.exists()
167 172
def is_user_on_whitelist(self, user): 168 173 def is_user_on_whitelist(self, user):
""" 169 174 """
:return: whether the user is on the waitlist for this section 170 175 :return: whether the user is on the waitlist for this section
""" 171 176 """
return self.whitelist.filter(email=user.email).exists() 172 177 return self.whitelist.filter(email=user.email).exists()
173 178
class Meta: 174 179 class Meta:
flashcards/serializers.py View file @ fe6a4ff
from django.utils.datetime_safe import datetime 1 1 from django.utils.datetime_safe import datetime
from flashcards.models import Section, LecturePeriod, User, Flashcard 2 2 from flashcards.models import Section, LecturePeriod, User, Flashcard
from rest_framework import serializers 3 3 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField 4 4 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField
from rest_framework.relations import HyperlinkedRelatedField 5 5 from rest_framework.relations import HyperlinkedRelatedField
from rest_framework.serializers import ModelSerializer, Serializer 6 6 from rest_framework.serializers import ModelSerializer, Serializer
from rest_framework.validators import UniqueValidator 7 7 from rest_framework.validators import UniqueValidator
8 8
9 9
class EmailSerializer(Serializer): 10 10 class EmailSerializer(Serializer):
email = EmailField(required=True) 11 11 email = EmailField(required=True)
12 12
13 13
class EmailPasswordSerializer(EmailSerializer): 14 14 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 15 15 password = CharField(required=True)
16 16
17 17
class RegistrationSerializer(EmailPasswordSerializer): 18 18 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 19 19 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
20 20
21 21
class PasswordResetRequestSerializer(EmailSerializer): 22 22 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 23 23 def validate_email(self, value):
try: 24 24 try:
User.objects.get(email=value) 25 25 User.objects.get(email=value)
return value 26 26 return value
except User.DoesNotExist: 27 27 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 28 28 raise serializers.ValidationError('No user exists with that email')
29 29
30 30
class PasswordResetSerializer(Serializer): 31 31 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 32 32 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 33 33 uid = IntegerField(required=True)
token = CharField(required=True) 34 34 token = CharField(required=True)
35 35
def validate_uid(self, value): 36 36 def validate_uid(self, value):
try: 37 37 try:
User.objects.get(id=value) 38 38 User.objects.get(id=value)
return value 39 39 return value
except User.DoesNotExist: 40 40 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 41 41 raise serializers.ValidationError('Could not verify reset token')
42 42
43 43
class UserUpdateSerializer(Serializer): 44 44 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 45 45 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 46 46 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 47 47 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 48 48 # reset_token = CharField(required=False)
49 49
def validate(self, data): 50 50 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 51 51 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 52 52 raise serializers.ValidationError('old_password is required to set a new_password')
return data 53 53 return data
54 54
55 55
class Password(Serializer): 56 56 class Password(Serializer):
email = EmailField(required=True) 57 57 email = EmailField(required=True)
password = CharField(required=True) 58 58 password = CharField(required=True)
59 59
60 60
class LecturePeriodSerializer(ModelSerializer): 61 61 class LecturePeriodSerializer(ModelSerializer):
class Meta: 62 62 class Meta:
model = LecturePeriod 63 63 model = LecturePeriod
exclude = 'id', 'section' 64 64 exclude = 'id', 'section'
65 65
66 66
class SectionSerializer(ModelSerializer): 67 67 class SectionSerializer(ModelSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 68 68 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
69 69
class Meta: 70 70 class Meta:
model = Section 71 71 model = Section
72 72
73 73
class UserSerializer(ModelSerializer): 74 74 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 75 75 email = EmailField(required=False)
sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail') 76 76 sections = HyperlinkedRelatedField(queryset=Section.objects.all(), many=True, view_name='section-detail')
is_confirmed = BooleanField() 77 77 is_confirmed = BooleanField()
78 78
class Meta: 79 79 class Meta:
model = User 80 80 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 81 81 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
82 82
83 83
class FlashcardSerializer(ModelSerializer): 84 84 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 85 85 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 86 86 hide_reason = CharField(read_only=True)
87 87
def validate_material_date(self, value): 88 88 def validate_material_date(self, value):
# TODO: make this dynamic 89 89 # TODO: make this dynamic
quarter_start = datetime(2015, 3, 15) 90 90 quarter_start = datetime(2015, 3, 15)
quarter_end = datetime(2015, 6, 15) 91 91 quarter_end = datetime(2015, 6, 15)
if quarter_start <= value <= quarter_end: return value 92 92 if quarter_start <= value <= quarter_end: return value
else: 93 93 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 94 94 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
flashcards/tests/test_models.py View file @ fe6a4ff
from django.test import TestCase 1 1 from django.test import TestCase
from flashcards.models import User, Section 2 2 from flashcards.models import User, Section, Flashcard
from simple_email_confirmation import EmailAddress 3 3 from datetime import datetime
4 4
5 5
class RegistrationTests(TestCase): 6 6 class RegistrationTests(TestCase):
def setUp(self): 7 7 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 8 8 User.objects.create_user(email="none@none.com", password="1234")
9 9
def test_email_confirmation(self): 10 10 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 11 11 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 12 12 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 13 13 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 14 14 self.assertTrue(user.is_confirmed)
15 15
16 16
class UserTests(TestCase): 17 17 class UserTests(TestCase):
def setUp(self): 18 18 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 19 19 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 20 20 Section.objects.create(department='dept',
course_num='101a', 21 21 course_num='101a',
course_title='how 2 test', 22 22 course_title='how 2 test',
instructor='George Lucas', 23 23 instructor='George Lucas',
quarter='SP15') 24 24 quarter='SP15')
25 25
def test_section_list(self): 26 26 def test_section_list(self):
section = Section.objects.get(course_num='101a') 27 27 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 28 28 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 29 29 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 30 30 user.sections.add(section)
self.assertIn(section, user.sections.all()) 31 31 self.assertIn(section, user.sections.all())
user.sections.add(section) 32 32 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 33 33 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 34 34 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 35 35 self.assertEqual(user.sections.count(), 0)
36
37
38 class FlashcardTests(TestCase):
39 def setUp(self):
40 user = User.objects.create_user(email="none@none.com", password="1234")
41 section = Section.objects.create(department='dept',
42 course_num='101a',
43 course_title='how 2 test',
44 instructor='George Lucas',
45 quarter='SP15')
46 Flashcard.objects.create(text="This is the text of the Flashcard",
47 section=section,
48 author=user,
49 material_date=datetime.now(),
50 previous=None,
51 mask={(0,4), (24,34)})
52
53 def test_mask_field(self):
54 user = User.objects.get(email="none@none.com")
55 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
56 self.assertTrue(isinstance(flashcard.mask, set))
flashcards/views.py View file @ fe6a4ff
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 3 3 from flashcards.models import Section, User, Flashcard, FlashcardReport
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
from rest_framework.decorators import detail_route, permission_classes, api_view 6 6 from rest_framework.decorators import detail_route, permission_classes, api_view
from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView 7 7 from rest_framework.generics import ListAPIView, DestroyAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 8 8 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin
from rest_framework.permissions import IsAuthenticated 9 9 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 10 10 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 11 11 from django.core.mail import send_mail
from django.contrib.auth import authenticate 12 12 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 13 13 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED 14 14 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED
from rest_framework.response import Response 15 15 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 16 16 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 17 17 from simple_email_confirmation import EmailAddress
18 18
19 19
class SectionViewSet(ReadOnlyModelViewSet): 20 20 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 21 21 queryset = Section.objects.all()
serializer_class = SectionSerializer 22 22 serializer_class = SectionSerializer
pagination_class = StandardResultsSetPagination 23 23 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 24 24 permission_classes = [IsAuthenticated]
25 25
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 26 26 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def enroll(self, request, pk): 27 27 def enroll(self, request, pk):
""" 28 28 """
Add the current user to a specified section 29 29 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. 30 30 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 31 31 ---
omit_serializer: true 32 32 omit_serializer: true
parameters: 33 33 parameters:
- fake: None 34 34 - fake: None
parameters_strategy: 35 35 parameters_strategy:
form: replace 36 36 form: replace
""" 37 37 """
section = self.get_object() 38 38 section = self.get_object()
if request.user.sections.filter(pk=section.pk).exists(): 39 39 if request.user.sections.filter(pk=section.pk).exists():
raise ValidationError("You are already in this section.") 40 40 raise ValidationError("You are already in this section.")
if section.is_whitelisted() and not section.is_user_on_whitelist(request.user): 41 41 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.") 42 42 raise PermissionDenied("You must be on the whitelist to add this section.")
request.user.sections.add(section) 43 43 request.user.sections.add(section)
return Response(status=HTTP_204_NO_CONTENT) 44 44 return Response(status=HTTP_204_NO_CONTENT)
45 45
@detail_route(methods=['post'], permission_classes=[IsAuthenticated]) 46 46 @detail_route(methods=['post'], permission_classes=[IsAuthenticated])
def drop(self, request, pk): 47 47 def drop(self, request, pk):
""" 48 48 """
Remove the current user from a specified section 49 49 Remove the current user from a specified section
If the user is not in the class, the request will fail. 50 50 If the user is not in the class, the request will fail.
--- 51 51 ---
omit_serializer: true 52 52 omit_serializer: true
parameters: 53 53 parameters:
- fake: None 54 54 - fake: None
parameters_strategy: 55 55 parameters_strategy:
form: replace 56 56 form: replace
""" 57 57 """
section = self.get_object() 58 58 section = self.get_object()
if not section.user_set.filter(pk=request.user.pk).exists(): 59 59 if not section.user_set.filter(pk=request.user.pk).exists():
raise ValidationError("You are not in the section.") 60 60 raise ValidationError("You are not in the section.")
section.user_set.remove(request.user) 61 61 section.user_set.remove(request.user)
return Response(status=HTTP_204_NO_CONTENT) 62 62 return Response(status=HTTP_204_NO_CONTENT)
63 63
64 64
class UserSectionListView(ListAPIView): 65 65 class UserSectionListView(ListAPIView):
serializer_class = SectionSerializer 66 66 serializer_class = SectionSerializer
permission_classes = [IsAuthenticated] 67 67 permission_classes = [IsAuthenticated]
68 68
def get_queryset(self): 69 69 def get_queryset(self):
return self.request.user.sections.all() 70 70 return self.request.user.sections.all()
71 71
def paginate_queryset(self, queryset): return None 72 72 def paginate_queryset(self, queryset): return None
73 73
74 74
class UserDetail(GenericAPIView): 75 75 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 76 76 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 77 77 permission_classes = [IsAuthenticated]
78 78
def get_queryset(self): 79 79 def get_queryset(self):
return User.objects.all() 80 80 return User.objects.all()
81 81
def patch(self, request, format=None): 82 82 def patch(self, request, format=None):
""" 83 83 """
Updates the user's password, or verifies their email address 84 84 Updates the user's password, or verifies their email address
--- 85 85 ---
request_serializer: UserUpdateSerializer 86 86 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 87 87 response_serializer: UserSerializer
""" 88 88 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 89 89 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 90 90 data.is_valid(raise_exception=True)
data = data.validated_data 91 91 data = data.validated_data
92 92
if 'new_password' in data: 93 93 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 94 94 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 95 95 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 96 96 request.user.set_password(data['new_password'])
request.user.save() 97 97 request.user.save()
98 98
if 'confirmation_key' in data: 99 99 if 'confirmation_key' in data:
try: 100 100 try:
request.user.confirm_email(data['confirmation_key']) 101 101 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 102 102 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 103 103 raise ValidationError('confirmation_key is invalid')
104 104
return Response(UserSerializer(request.user).data) 105 105 return Response(UserSerializer(request.user).data)
106 106
def get(self, request, format=None): 107 107 def get(self, request, format=None):
""" 108 108 """
Return data about the user 109 109 Return data about the user
--- 110 110 ---
response_serializer: UserSerializer 111 111 response_serializer: UserSerializer
""" 112 112 """
serializer = UserSerializer(request.user, context={'request': request}) 113 113 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 114 114 return Response(serializer.data)
115 115
def delete(self, request): 116 116 def delete(self, request):
""" 117 117 """
Irrevocably delete the user and their data 118 118 Irrevocably delete the user and their data
119 119
Yes, really 120 120 Yes, really
""" 121 121 """
request.user.delete() 122 122 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 123 123 return Response(status=HTTP_204_NO_CONTENT)
124 124
@api_view(['POST']) 125 125 @api_view(['POST'])
def register(request, format=None): 126 126 def register(request, format=None):
""" 127 127 """
Register a new user 128 128 Register a new user
--- 129 129 ---
request_serializer: EmailPasswordSerializer 130 130 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 131 131 response_serializer: UserSerializer
""" 132 132 """
data = RegistrationSerializer(data=request.data) 133 133 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 134 134 data.is_valid(raise_exception=True)
135 135
User.objects.create_user(**data.validated_data) 136 136 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 137 137 user = authenticate(**data.validated_data)
auth.login(request, user) 138 138 auth.login(request, user)
139 139
body = ''' 140 140 body = '''
Visit the following link to confirm your email address: 141 141 Visit the following link to confirm your email address:
https://flashy.cards/app/verify_email/%s 142 142 https://flashy.cards/app/verify_email/%s
143 143
If you did not register for Flashy, no action is required. 144 144 If you did not register for Flashy, no action is required.
''' 145 145 '''
146 146
assert send_mail("Flashy email verification", 147 147 assert send_mail("Flashy email verification",
body % user.confirmation_key, 148 148 body % user.confirmation_key,
"noreply@flashy.cards", 149 149 "noreply@flashy.cards",
[user.email]) 150 150 [user.email])
151 151
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 152 152 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
153 153
@api_view(['POST']) 154 154 @api_view(['POST'])
def login(request): 155 155 def login(request):
""" 156 156 """
Authenticates user and returns user data if valid. 157 157 Authenticates user and returns user data if valid.
--- 158 158 ---
request_serializer: EmailPasswordSerializer 159 159 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 160 160 response_serializer: UserSerializer
""" 161 161 """
162 162
data = EmailPasswordSerializer(data=request.data) 163 163 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 164 164 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 165 165 user = authenticate(**data.validated_data)
166 166
if user is None: 167 167 if user is None:
raise AuthenticationFailed('Invalid email or password') 168 168 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 169 169 if not user.is_active:
raise NotAuthenticated('Account is disabled') 170 170 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 171 171 auth.login(request, user)
return Response(UserSerializer(request.user).data) 172 172 return Response(UserSerializer(request.user).data)
173 173
174 174
@api_view(['POST']) 175 175 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 176 176 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 177 177 def logout(request, format=None):
""" 178 178 """
Logs the authenticated user out. 179 179 Logs the authenticated user out.
""" 180 180 """
auth.logout(request) 181 181 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 182 182 return Response(status=HTTP_204_NO_CONTENT)
183 183
184 184
@api_view(['POST']) 185 185 @api_view(['POST'])
def request_password_reset(request, format=None): 186 186 def request_password_reset(request, format=None):
""" 187 187 """
Send a password reset token/link to the provided email. 188 188 Send a password reset token/link to the provided email.
--- 189 189 ---
request_serializer: PasswordResetRequestSerializer 190 190 request_serializer: PasswordResetRequestSerializer
""" 191 191 """
data = PasswordResetRequestSerializer(data=request.data) 192 192 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 193 193 data.is_valid(raise_exception=True)
user = User.objects.get(email=data['email'].value) 194 194 user = User.objects.get(email=data['email'].value)
token = default_token_generator.make_token(user) 195 195 token = default_token_generator.make_token(user)
196 196
body = ''' 197 197 body = '''
Visit the following link to reset your password: 198 198 Visit the following link to reset your password:
https://flashy.cards/app/reset_password/%d/%s 199 199 https://flashy.cards/app/reset_password/%d/%s
200 200
If you did not request a password reset, no action is required. 201 201 If you did not request a password reset, no action is required.
''' 202 202 '''
203 203
send_mail("Flashy password reset", 204 204 send_mail("Flashy password reset",
body % (user.pk, token), 205 205 body % (user.pk, token),
"noreply@flashy.cards", 206 206 "noreply@flashy.cards",
[user.email]) 207 207 [user.email])
208 208
return Response(status=HTTP_204_NO_CONTENT) 209 209 return Response(status=HTTP_204_NO_CONTENT)
210 210
211 211
@api_view(['POST']) 212 212 @api_view(['POST'])
def reset_password(request, format=None): 213 213 def reset_password(request, format=None):
""" 214 214 """
Updates user's password to new password if token is valid. 215 215 Updates user's password to new password if token is valid.
--- 216 216 ---
request_serializer: PasswordResetSerializer 217 217 request_serializer: PasswordResetSerializer
""" 218 218 """
data = PasswordResetSerializer(data=request.data) 219 219 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 220 220 data.is_valid(raise_exception=True)