Commit 2f49f8258704c96bd02d13a3a0af1a947aff981f

Authored by Laura Hawkins
Exists in master

Merge branch 'master' of https://git.ucsd.edu/110swag/flashy-backend

Conflicts:
	flashcards/views.py

Showing 12 changed files Inline Diff

flashcards/api.py View file @ 2f49f82
from flashcards.models import Flashcard 1 1 from flashcards.models import Flashcard, UserFlashcardQuiz
from rest_framework.pagination import PageNumberPagination 2 2 from rest_framework.pagination import PageNumberPagination
from rest_framework.permissions import BasePermission 3 3 from rest_framework.permissions import BasePermission
4 4
5 5
mock_no_params = lambda x:None 6 6 mock_no_params = lambda x:None
7 7
class StandardResultsSetPagination(PageNumberPagination): 8 8 class StandardResultsSetPagination(PageNumberPagination):
page_size = 40 9 9 page_size = 40
page_size_query_param = 'page_size' 10 10 page_size_query_param = 'page_size'
max_page_size = 1000 11 11 max_page_size = 1000
12 12
13 13
class UserDetailPermissions(BasePermission): 14 14 class UserDetailPermissions(BasePermission):
""" 15 15 """
Permissions for the user detail view. Anonymous users may only POST. 16 16 Permissions for the user detail view. Anonymous users may only POST.
""" 17 17 """
18 18
def has_object_permission(self, request, view, obj): 19 19 def has_object_permission(self, request, view, obj):
if request.method == 'POST': 20 20 if request.method == 'POST':
return True 21 21 return True
return request.user.is_authenticated() 22 22 return request.user.is_authenticated()
23 23
24 24
class IsEnrolledInAssociatedSection(BasePermission): 25 25 class IsEnrolledInAssociatedSection(BasePermission):
def has_object_permission(self, request, view, obj): 26 26 def has_object_permission(self, request, view, obj):
27 if obj is None:
28 return True
assert type(obj) is Flashcard 27 29 assert type(obj) is Flashcard
return request.user.is_in_section(obj.section) 28 30 return request.user.is_in_section(obj.section)
31
flashcards/fields.py View file @ 2f49f82
from django.db import models 1 1 from django.db import models
from validators import FlashcardMask, OverlapIntervalException 2 2 from validators import FlashcardMask, OverlapIntervalException
3 3
4 4
class MaskField(models.Field): 5 5 class MaskField(models.Field):
def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs): 6 6 def __init__(self, blank_sep=',', range_sep='-', *args, **kwargs):
self.blank_sep = blank_sep 7 7 self.blank_sep = blank_sep
self.range_sep = range_sep 8 8 self.range_sep = range_sep
super(MaskField, self).__init__(*args, **kwargs) 9 9 super(MaskField, self).__init__(*args, **kwargs)
10 10
@staticmethod 11 11 @staticmethod
def _using_array(connection): 12 12 def _using_array(connection):
return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2' 13 13 return connection.settings_dict['ENGINE'] == 'django.db.backends.postgresql_psycopg2'
14 14
def deconstruct(self): 15 15 def deconstruct(self):
name, path, args, kwargs = super(MaskField, self).deconstruct() 16 16 name, path, args, kwargs = super(MaskField, self).deconstruct()
kwargs['blank_sep'] = self.blank_sep 17 17 kwargs['blank_sep'] = self.blank_sep
kwargs['range_sep'] = self.range_sep 18 18 kwargs['range_sep'] = self.range_sep
return name, path, args, kwargs 19 19 return name, path, args, kwargs
20 20
def db_type(self, connection): 21 21 def db_type(self, connection):
return 'integer[2][]' if self._using_array(connection) else 'varchar' 22 22 return 'integer[2][]' if self._using_array(connection) else 'varchar'
23 23
def from_db_value(self, value, expression, connection, context): 24 24 def from_db_value(self, value, expression, connection, context):
if value is None: 25 25 if value is None:
return value 26 26 return value
if self._using_array(connection): 27 27 if self._using_array(connection):
return MaskField._psql_parse_mask(value) 28 28 return MaskField._psql_parse_mask(value)
return MaskField._varchar_parse_mask(value) 29 29 return MaskField._varchar_parse_mask(value)
30 30
def get_db_prep_value(self, value, connection, prepared=False): 31 31 def get_db_prep_value(self, value, connection, prepared=False):
if not prepared: 32 32 if not prepared:
value = self.get_prep_value(value) 33 33 value = self.get_prep_value(value)
if value is None: 34 34 if value is None:
return value 35 35 return value
if self._using_array(connection): 36 36 if self._using_array(connection):
return value 37 37 return value
return ','.join(['-'.join(map(str, i)) for i in value]) 38 38 return ','.join(['-'.join(map(str, i)) for i in value])
39 39
def to_python(self, value): 40 40 def to_python(self, value):
if value is None: 41 41 if value is None:
return value 42 42 return value
return sorted(list(FlashcardMask(value))) 43 43 return FlashcardMask(value)
44 44
def get_prep_value(self, value): 45 45 def get_prep_value(self, value):
if value is None: 46 46 if value is None:
return value 47 47 return value
return sorted(map(list, FlashcardMask(value))) 48 48 return sorted(map(list, FlashcardMask(value)))
49 49
def get_prep_lookup(self, lookup_type, value): 50 50 def get_prep_lookup(self, lookup_type, value):
raise TypeError("Lookup not supported for MaskField") 51 51 raise TypeError("Lookup not supported for MaskField")
52 52
@staticmethod 53 53 @staticmethod
def _parse_mask(intervals): 54 54 def _parse_mask(intervals):
p_beg, p_end = -1, -1 55 55 p_beg, p_end = -1, -1
mask_list = [] 56 56 mask_list = []
for interval in intervals: 57 57 for interval in intervals:
beg, end = map(int, interval) 58 58 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 59 59 if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end):
raise ValueError("Invalid range offsets in the mask") 60 60 raise ValueError("Invalid range offsets in the mask")
mask_list.append([beg, end]) 61 61 mask_list.append([beg, end])
p_beg, p_end = beg, end 62 62 p_beg, p_end = beg, end
return mask_list 63 63 return mask_list
flashcards/models.py View file @ 2f49f82
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 2 2 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 3 3 from django.core.cache import cache
from django.core.exceptions import ValidationError 4 4 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 5 5 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 6 6 from django.core.mail import send_mail
from django.db import IntegrityError 7 7 from django.db import IntegrityError
from django.db.models import * 8 8 from django.db.models import *
from django.utils.timezone import now 9 9 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 10 10 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 11 11 from fields import MaskField
12 12
13 13
# Hack to fix AbstractUser before subclassing it 14 14 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 15 15 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 16 16 AbstractUser._meta.get_field('username')._unique = False
17 17
18 18
class EmailOnlyUserManager(UserManager): 19 19 class EmailOnlyUserManager(UserManager):
""" 20 20 """
A tiny extension of Django's UserManager which correctly creates users 21 21 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 22 22 without usernames (using emails instead).
""" 23 23 """
24 24
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 25 25 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 26 26 """
Creates and saves a User with the given email and password. 27 27 Creates and saves a User with the given email and password.
""" 28 28 """
email = self.normalize_email(email) 29 29 email = self.normalize_email(email)
user = self.model(email=email, 30 30 user = self.model(email=email,
is_staff=is_staff, is_active=True, 31 31 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 32 32 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 33 33 date_joined=now(), **extra_fields)
user.set_password(password) 34 34 user.set_password(password)
user.save(using=self._db) 35 35 user.save(using=self._db)
return user 36 36 return user
37 37
def create_user(self, email, password=None, **extra_fields): 38 38 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 39 39 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 40 40 body = '''
Visit the following link to confirm your email address: 41 41 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 42 42 https://flashy.cards/app/verifyemail/%s
43 43
If you did not register for Flashy, no action is required. 44 44 If you did not register for Flashy, no action is required.
''' 45 45 '''
46 46
assert send_mail("Flashy email verification", 47 47 assert send_mail("Flashy email verification",
body % user.confirmation_key, 48 48 body % user.confirmation_key,
"noreply@flashy.cards", 49 49 "noreply@flashy.cards",
[user.email]) 50 50 [user.email])
return user 51 51 return user
52 52
def create_superuser(self, email, password, **extra_fields): 53 53 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 54 54 return self._create_user(email, password, True, True, **extra_fields)
55 55
56 56
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 57 57 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 58 58 """
An extension of Django's default user model. 59 59 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 60 60 We use email as the username field, and include enrolled sections here
""" 61 61 """
objects = EmailOnlyUserManager() 62 62 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 63 63 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 64 64 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 65 65 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
66 66
def is_in_section(self, section): 67 67 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 68 68 return self.sections.filter(pk=section.pk).exists()
69 69
def pull(self, flashcard): 70 70 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 71 71 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 72 72 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 73 73 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 74 74 user_card.pulled = now()
user_card.save() 75 75 user_card.save()
76 76
def unpull(self, flashcard): 77 77 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 78 78 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 79 79 raise ValueError("User not in the section this flashcard belongs to")
80 80
try: 81 81 try:
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 82 82 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
except UserFlashcard.DoesNotExist: 83 83 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 84 84 raise ValueError('Cannot unpull card that is not pulled.')
85 85
user_card.delete() 86 86 user_card.delete()
87 87
def get_deck(self, section): 88 88 def get_deck(self, section):
if not self.is_in_section(section): 89 89 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 90 90 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 91 91 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
92 92
def request_password_reset(self): 93 93 def request_password_reset(self):
token = default_token_generator.make_token(self) 94 94 token = default_token_generator.make_token(self)
95 95
body = ''' 96 96 body = '''
Visit the following link to reset your password: 97 97 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 98 98 https://flashy.cards/app/resetpassword/%d/%s
99 99
If you did not request a password reset, no action is required. 100 100 If you did not request a password reset, no action is required.
''' 101 101 '''
102 102
send_mail("Flashy password reset", 103 103 send_mail("Flashy password reset",
body % (self.pk, token), 104 104 body % (self.pk, token),
"noreply@flashy.cards", 105 105 "noreply@flashy.cards",
[self.email]) 106 106 [self.email])
107 107
108 108
class UserFlashcard(Model): 109 109 class UserFlashcard(Model):
""" 110 110 """
Represents the relationship between a user and a flashcard by: 111 111 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 112 112 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 113 113 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 114 114 3. A user has a flashcard hidden from them
""" 115 115 """
user = ForeignKey('User') 116 116 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 117 117 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 118 118 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") 119 119 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 120 120 flashcard = ForeignKey('Flashcard')
121 121
class Meta: 122 122 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 123 123 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 124 124 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 125 125 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 126 126 # By default, order by most recently pulled
ordering = ['-pulled'] 127 127 ordering = ['-pulled']
128 128
129 129
class FlashcardHide(Model): 130 130 class FlashcardHide(Model):
""" 131 131 """
Represents the property of a flashcard being hidden by a user. 132 132 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 133 133 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 134 134 If reason is null, the flashcard was just hidden.
If reason is not null, the flashcard was reported, and reason is the reason why it was reported. 135 135 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 136 136 """
user = ForeignKey('User') 137 137 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 138 138 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 139 139 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 140 140 hidden = DateTimeField(auto_now_add=True)
141 141
class Meta: 142 142 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 143 143 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 144 144 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 145 145 index_together = ["user", "flashcard"]
146 146
147 147
class Flashcard(Model): 148 148 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 149 149 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') 150 150 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") 151 151 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") 152 152 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 153 153 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 154 154 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 155 155 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 156 156 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 157 157 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 158 158 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 159 159 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
160 160
class Meta: 161 161 class Meta:
# By default, order by most recently pushed 162 162 # By default, order by most recently pushed
ordering = ['-pushed'] 163 163 ordering = ['-pushed']
164 164
def is_hidden_from(self, user): 165 165 def is_hidden_from(self, user):
""" 166 166 """
A card can be hidden globally, but if a user has the card in their deck, 167 167 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 168 168 this visibility overrides a global hide.
:param user: 169 169 :param user:
:return: Whether the card is hidden from the user. 170 170 :return: Whether the card is hidden from the user.
""" 171 171 """
if self.userflashcard_set.filter(user=user).exists(): return False 172 172 if self.userflashcard_set.filter(user=user).exists(): return False
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 173 173 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return False 174 174 return False
175 175
def hide_from(self, user, reason=None): 176 176 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 177 177 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 178 178 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 179 179 if not created:
raise ValidationError("The card has already been hidden.") 180 180 raise ValidationError("The card has already been hidden.")
obj.save() 181 181 obj.save()
182 182
def is_in_deck(self, user): 183 183 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 184 184 return self.userflashcard_set.filter(user=user).exists()
185 185
def add_to_deck(self, user): 186 186 def add_to_deck(self, user):
if not user.is_in_section(self.section): 187 187 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 188 188 raise PermissionDenied("You don't have the permission to add this card")
try: 189 189 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 190 190 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 191 191 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 192 192 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 193 193 user_flashcard.save()
return user_flashcard 194 194 return user_flashcard
195 195
def edit(self, user, new_data): 196 196 def edit(self, user, new_data):
""" 197 197 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 198 198 Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard.
Sets up everything correctly so this object, when saved, will result in the appropriate changes. 199 199 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 200 200 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 201 201 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 202 202 """
203 203
# content_changed is True iff either material_date or text were changed 204 204 # content_changed is True iff either material_date or text were changed
content_changed = False 205 205 content_changed = False
# create_new is True iff the user editing this card is the author of this card 206 206 # create_new is True iff the user editing this card is the author of this card
# and there are no other users with this card in their decks 207 207 # and there are no other users with this card in their decks
create_new = user != self.author or \ 208 208 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 209 209 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 210 210 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 211 211 content_changed = True
self.material_date = new_data['material_date'] 212 212 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 213 213 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 214 214 content_changed = True
self.text = new_data['text'] 215 215 self.text = new_data['text']
if create_new and content_changed: 216 216 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 217 217 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 218 218 self.previous_id = self.pk
self.pk = None 219 219 self.pk = None
self.mask = new_data.get('mask', self.mask) 220 220 self.mask = new_data.get('mask', self.mask)
self.save() 221 221 self.save()
self.add_to_deck(user) 222 222 self.add_to_deck(user)
else: 223 223 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 224 224 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 225 225 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 226 226 user_card.save()
return self 227 227 return self
228 228
def report(self, user, reason=None): 229 229 def report(self, user, reason=None):
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 230 230 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 231 231 obj.reason = reason
obj.save() 232 232 obj.save()
233 233
@classmethod 234 234 @classmethod
def cards_visible_to(cls, user): 235 235 def cards_visible_to(cls, user):
""" 236 236 """
:param user: 237 237 :param user:
:return: A queryset with all cards that should be visible to a user. 238 238 :return: A queryset with all cards that should be visible to a user.
""" 239 239 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 240 240 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
241 241
242 242
class UserFlashcardQuiz(Model): 243 243 class UserFlashcardQuiz(Model):
""" 244 244 """
An event of a user being quizzed on a flashcard. 245 245 An event of a user being quizzed on a flashcard.
""" 246 246 """
user_flashcard = ForeignKey(UserFlashcard) 247 247 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 248 248 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 249 249 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") 250 250 response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 251 251 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
252 252
def status(self): 253 253 def status(self):
""" 254 254 """
There are three stages of a quiz object: 255 255 There are three stages of a quiz object:
1. the user has been shown the card 256 256 1. the user has been shown the card
2. the user has answered the card 257 257 2. the user has answered the card
3. the user has self-evaluated their response's correctness 258 258 3. the user has self-evaluated their response's correctness
259 259
:return: string (evaluated, answered, viewed) 260 260 :return: string (evaluated, answered, viewed)
""" 261 261 """
if self.correct is not None: return "evaluated" 262 262 if self.correct is not None: return "evaluated"
if self.response: return "answered" 263 263 if self.response: return "answered"
return "viewed" 264 264 return "viewed"
265 265
266 266
class Section(Model): 267 267 class Section(Model):
""" 268 268 """
A UCSD course taught by an instructor during a quarter. 269 269 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 270 270 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 271 271 We index gratuitously to support autofill and because this is primarily read-only
""" 272 272 """
department = CharField(db_index=True, max_length=50) 273 273 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 274 274 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 275 275 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 276 276 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 277 277 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 278 278 quarter = CharField(db_index=True, max_length=4)
279 279
@classmethod 280 280 @classmethod
def search(cls, terms): 281 281 def search(cls, terms):
""" 282 282 """
Search all fields of all sections for a particular set of terms 283 283 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 284 284 A matching section must match at least one field on each term
:param terms:iterable 285 285 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 286 286 :return: Matching QuerySet ordered by department and course number
""" 287 287 """
final_q = Q() 288 288 final_q = Q()
for term in terms: 289 289 for term in terms:
q = Q(department__icontains=term) 290 290 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 291 291 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 292 292 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 293 293 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 294 294 q |= Q(instructor__icontains=term)
final_q &= q 295 295 final_q &= q
qs = cls.objects.filter(final_q) 296 296 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 297 297 # 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 298 298 # 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)"}) 299 299 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 300 300 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 301 301 return qs
302 302
@property 303 303 @property
def is_whitelisted(self): 304 304 def is_whitelisted(self):
""" 305 305 """
:return: whether a whitelist exists for this section 306 306 :return: whether a whitelist exists for this section
""" 307 307 """
return self.whitelist.exists() 308 308 return self.whitelist.exists()
309 309
def is_user_on_whitelist(self, user): 310 310 def is_user_on_whitelist(self, user):
""" 311 311 """
:return: whether the user is on the waitlist for this section 312 312 :return: whether the user is on the waitlist for this section
""" 313 313 """
return self.whitelist.filter(email=user.email).exists() 314 314 return self.whitelist.filter(email=user.email).exists()
315 315
316
def enroll(self, user): 317 316 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 318 317 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 319 318 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 320 319 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 321 320 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 322 321 self.user_set.add(user)
323 322
def drop(self, user): 324 323 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 325 324 if not user.sections.filter(pk=self.pk).exists():
raise ValidationError("User is not enrolled in the section.") 326 325 raise ValidationError("User is not enrolled in the section.")
self.user_set.remove(user) 327 326 self.user_set.remove(user)
328 327
class Meta: 329 328 class Meta:
ordering = ['-course_title'] 330 329 ordering = ['department_abbreviation', 'course_num']
331 330
@property 332 331 @property
def lecture_times(self): 333 332 def lecture_times(self):
data = cache.get("section_%d_lecture_times" % self.pk) 334 333 data = cache.get("section_%d_lecture_times" % self.pk)
flashcards/serializers.py View file @ 2f49f82
from json import dumps, loads 1 1 from json import dumps, loads
2 2
from django.utils.datetime_safe import datetime 3 3 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 4 4 from django.utils.timezone import now
import pytz 5 5 import pytz
from flashcards.models import Section, LecturePeriod, User, Flashcard 6 6 from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 7 7 from flashcards.validators import FlashcardMask, OverlapIntervalException
from rest_framework import serializers 8 8 from rest_framework import serializers
from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField 9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty
from rest_framework.serializers import ModelSerializer, Serializer 10 10 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 11 11 from rest_framework.validators import UniqueValidator
12 from flashy.settings import QUARTER_END, QUARTER_START
13 from random import sample
12 14
13 15
class EmailSerializer(Serializer): 14 16 class EmailSerializer(Serializer):
email = EmailField(required=True) 15 17 email = EmailField(required=True)
16 18
17 19
class EmailPasswordSerializer(EmailSerializer): 18 20 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 19 21 password = CharField(required=True)
20 22
21 23
class RegistrationSerializer(EmailPasswordSerializer): 22 24 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 23 25 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
24 26
25 27
class PasswordResetRequestSerializer(EmailSerializer): 26 28 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 27 29 def validate_email(self, value):
try: 28 30 try:
User.objects.get(email=value) 29 31 User.objects.get(email=value)
return value 30 32 return value
except User.DoesNotExist: 31 33 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 32 34 raise serializers.ValidationError('No user exists with that email')
33 35
34 36
class PasswordResetSerializer(Serializer): 35 37 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 36 38 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 37 39 uid = IntegerField(required=True)
token = CharField(required=True) 38 40 token = CharField(required=True)
39 41
def validate_uid(self, value): 40 42 def validate_uid(self, value):
try: 41 43 try:
User.objects.get(id=value) 42 44 User.objects.get(id=value)
return value 43 45 return value
except User.DoesNotExist: 44 46 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 45 47 raise serializers.ValidationError('Could not verify reset token')
46 48
47 49
class UserUpdateSerializer(Serializer): 48 50 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 49 51 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 50 52 new_password = CharField(required=False, allow_blank=False)
confirmation_key = CharField(required=False) 51 53 confirmation_key = CharField(required=False)
# reset_token = CharField(required=False) 52 54 # reset_token = CharField(required=False)
53 55
def validate(self, data): 54 56 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 55 57 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 56 58 raise serializers.ValidationError('old_password is required to set a new_password')
return data 57 59 return data
58 60
59 61
class Password(Serializer): 60 62 class Password(Serializer):
email = EmailField(required=True) 61 63 email = EmailField(required=True)
password = CharField(required=True) 62 64 password = CharField(required=True)
63 65
64 66
class LecturePeriodSerializer(ModelSerializer): 65 67 class LecturePeriodSerializer(ModelSerializer):
class Meta: 66 68 class Meta:
model = LecturePeriod 67 69 model = LecturePeriod
exclude = 'id', 'section' 68 70 exclude = 'id', 'section'
69 71
70 72
class SectionSerializer(ModelSerializer): 71 73 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 72 74 lecture_times = CharField()
short_name = CharField() 73 75 short_name = CharField()
long_name = CharField() 74 76 long_name = CharField()
75 77
class Meta: 76 78 class Meta:
model = Section 77 79 model = Section
78 80
class DeepSectionSerializer(SectionSerializer): 79 81 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 80 82 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
81 83
82 84
83 85
class UserSerializer(ModelSerializer): 84 86 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 85 87 email = EmailField(required=False)
sections = SectionSerializer(many=True) 86 88 sections = SectionSerializer(many=True)
is_confirmed = BooleanField() 87 89 is_confirmed = BooleanField()
88 90
class Meta: 89 91 class Meta:
model = User 90 92 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined") 91 93 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined")
92 94
93 95
class MaskFieldSerializer(serializers.Field): 94 96 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 95 97 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 96 98 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 97 99 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 98 100 'overlap': 'Ensure this field does not have overlapping intervals.'
} 99 101 }
100 102
def to_representation(self, value): 101 103 def to_representation(self, value):
return dumps(list(self._make_mask(value))) 102 104 return dumps(list(self._make_mask(value)))
103 105
def to_internal_value(self, value): 104 106 def to_internal_value(self, value):
return self._make_mask(loads(value)) 105 107 return self._make_mask(loads(value))
106 108
def _make_mask(self, data): 107 109 def _make_mask(self, data):
try: 108 110 try:
mask = FlashcardMask(data) 109 111 mask = FlashcardMask(data)
except ValueError: 110 112 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 111 113 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 112 114 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 113 115 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 114 116 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 115 117 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 116 118 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 117 119 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 118 120 return mask
119 121
120 122
class FlashcardSerializer(ModelSerializer): 121 123 class FlashcardSerializer(ModelSerializer):
is_hidden = BooleanField(read_only=True) 122 124 is_hidden = BooleanField(read_only=True)
hide_reason = CharField(read_only=True) 123 125 hide_reason = CharField(read_only=True)
material_date = DateTimeField(default=now) 124 126 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 125 127 mask = MaskFieldSerializer(allow_null=True)
126 128
def validate_material_date(self, value): 127 129 def validate_material_date(self, value):
utc = pytz.UTC 128
# TODO: make this dynamic 129 130 # TODO: make this dynamic
quarter_start = utc.localize(datetime(2015, 3, 15)) 130 131 if QUARTER_START <= value <= QUARTER_END:
quarter_end = utc.localize(datetime(2015, 6, 15)) 131
132
if quarter_start <= value <= quarter_end: 133
return value 134 132 return value
else: 135 133 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 136 134 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
137 135
def validate_pushed(self, value): 138 136 def validate_pushed(self, value):
if value > datetime.now(): 139 137 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 140 138 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 141 139 return value
142 140
def validate_mask(self, value): 143 141 def validate_mask(self, value):
if value is None: 144 142 if value is None:
return None 145 143 return None
if len(self.initial_data['text']) < value.max_offset(): 146 144 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 147 145 raise serializers.ValidationError("Mask out of bounds")
return value 148 146 return value
149 147
class Meta: 150 148 class Meta:
model = Flashcard 151 149 model = Flashcard
exclude = 'author', 'previous' 152 150 exclude = 'author', 'previous'
153 151
154 152
class FlashcardUpdateSerializer(serializers.Serializer): 155 153 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 156 154 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 157 155 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 158 156 mask = MaskFieldSerializer(required=False)
159 157
def validate_material_date(self, date): 160 158 def validate_material_date(self, date):
quarter_end = pytz.UTC.localize(datetime(2015, 6, 15)) 161 159 if date > QUARTER_END:
if date > quarter_end: 162
raise serializers.ValidationError("Invalid material_date for the flashcard") 163 160 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 164 161 return date
165 162
def validate(self, attrs): 166 163 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 167 164 # Make sure that at least one of the attributes was passed in
if not any(i in attrs for i in ['material_date', 'text', 'mask']): 168 165 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 169 166 raise serializers.ValidationError("No new value passed in")
return attrs 170 167 return attrs
168
169
170 class QuizRequestSerializer(serializers.Serializer):
171 # sections = PrimaryKeyRelatedField(queryset=Section.objects.all(),required=False, many=True)
172 sections = ListField(child=IntegerField(min_value=1), required=False)
173 material_date_begin = DateTimeField(default=QUARTER_START)
174 material_date_end = DateTimeField(default=QUARTER_END)
175
176 def __init__(self, user, *args, **kwargs):
177 super(QuizRequestSerializer, self).__init__(*args, **kwargs)
178 self.user = user
179 self.user_flashcard = None
180
181 def create(self, validated_data):
182 return UserFlashcardQuiz.objects.create(user_flashcard=self.user_flashcard)
183
184 def update(self, instance, validated_data):
185 for attr in validated_data:
186 setattr(instance, attr, validated_data[attr])
187 instance.save()
188 return instance
189
190 def _get_user_flashcard(self, attrs):
191 user_flashcard_filter = UserFlashcard.objects.filter(
192 user=self.user, flashcard__section__in=attrs['sections'],
193 flashcard__material_date__gte=attrs['material_date_begin'],
194 flashcard__material_date__lte=attrs['material_date_end']
195 )
196 if not user_flashcard_filter.exists():
197 raise serializers.ValidationError("Your deck for that section is empty")
198 self.user_flashcard = user_flashcard_filter.order_by('?').first()
199
200 def validate_material_date_begin(self, value):
201 if QUARTER_START <= value <= QUARTER_END:
202 return value
203 raise serializers.ValidationError("Invalid begin date for the flashcard range")
204
205 def validate_material_date_end(self, value):
206 if QUARTER_START <= value <= QUARTER_END:
207 return value
208 raise serializers.ValidationError("Invalid end date for the flashcard range")
209
210 def validate_sections(self, value):
211 if value is None:
212 return self.user.sections
213 section_filter = Section.objects.filter(pk__in=value)
214 if not section_filter.exists():
215 raise serializers.ValidationError("You aren't enrolled in those section(s)")
216 return section_filter
217
218 def validate(self, attrs):
219 if attrs['material_date_begin'] > attrs['material_date_end']:
220 raise serializers.ValidationError("Invalid range")
221 if 'sections' not in attrs:
222 attrs['sections'] = self.validate_sections(None)
223 self._get_user_flashcard(attrs)
224 return attrs
225
226
227 class QuizResponseSerializer(ModelSerializer):
228 pk = PrimaryKeyRelatedField(queryset=UserFlashcardQuiz.objects.all(), many=True)
229 section = PrimaryKeyRelatedField(queryset=Section.objects.all())
230 text = CharField(max_length=255)
231 mask = ListField(child=IntegerField())
232
233 def __init__(self, instance=None, mask=[], data=empty, **kwargs):
234 super(QuizResponseSerializer, self).__init__(instance=instance, data=data, **kwargs)
235 self.mask = self._validate_mask(mask)
236
237 def to_representation(self, instance):
238 return {
239 'pk': instance.user_flashcard.pk,
240 'section': instance.user_flashcard.flashcard.section.pk,
241 'text': instance.user_flashcard.flashcard.text,
242 'mask': self.mask
243 }
244
245 def _validate_mask(self, value):
246 if not isinstance(value, tuple) and value is not None:
247 raise serializers.ValidationError("The selected mask has to be a list")
flashcards/tests/test_api.py View file @ 2f49f82
from django.core import mail 1 1 from django.core import mail
from flashcards.models import * 2 2 from flashcards.models import *
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN 3 3 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK, HTTP_403_FORBIDDEN
from rest_framework.test import APITestCase 4 4 from rest_framework.test import APITestCase
from re import search 5 5 from re import search
import datetime 6 6 import datetime
from django.utils.timezone import now 7 7 from django.utils.timezone import now
from flashcards.validators import FlashcardMask 8 8 from flashcards.validators import FlashcardMask
from flashcards.serializers import FlashcardSerializer 9 9 from flashcards.serializers import FlashcardSerializer
10 10
11 11
class LoginTests(APITestCase): 12 12 class LoginTests(APITestCase):
fixtures = ['testusers'] 13 13 fixtures = ['testusers']
14 14
def test_login(self): 15 15 def test_login(self):
url = '/api/login/' 16 16 url = '/api/login/'
data = {'email': 'none@none.com', 'password': '1234'} 17 17 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 18 18 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 19 19 self.assertEqual(response.status_code, HTTP_200_OK)
20 20
data = {'email': 'none@none.com', 'password': '4321'} 21 21 data = {'email': 'none@none.com', 'password': '4321'}
response = self.client.post(url, data, format='json') 22 22 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 23 23 self.assertContains(response, 'Invalid email or password', status_code=403)
24 24
data = {'email': 'bad@none.com', 'password': '1234'} 25 25 data = {'email': 'bad@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 26 26 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Invalid email or password', status_code=403) 27 27 self.assertContains(response, 'Invalid email or password', status_code=403)
28 28
data = {'password': '4321'} 29 29 data = {'password': '4321'}
response = self.client.post(url, data, format='json') 30 30 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 31 31 self.assertContains(response, 'email', status_code=400)
32 32
data = {'email': 'none@none.com'} 33 33 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 34 34 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 35 35 self.assertContains(response, 'password', status_code=400)
36 36
user = User.objects.get(email="none@none.com") 37 37 user = User.objects.get(email="none@none.com")
user.is_active = False 38 38 user.is_active = False
user.save() 39 39 user.save()
40 40
data = {'email': 'none@none.com', 'password': '1234'} 41 41 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 42 42 response = self.client.post(url, data, format='json')
self.assertContains(response, 'Account is disabled', status_code=403) 43 43 self.assertContains(response, 'Account is disabled', status_code=403)
44 44
def test_logout(self): 45 45 def test_logout(self):
self.client.login(email='none@none.com', password='1234') 46 46 self.client.login(email='none@none.com', password='1234')
response = self.client.post('/api/logout/') 47 47 response = self.client.post('/api/logout/')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 48 48 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
49 49
# since we're not logged in, we should get a 403 response 50 50 # since we're not logged in, we should get a 403 response
response = self.client.get('/api/me/', format='json') 51 51 response = self.client.get('/api/me/', format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 52 52 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
53 53
54 54
class PasswordResetTest(APITestCase): 55 55 class PasswordResetTest(APITestCase):
fixtures = ['testusers'] 56 56 fixtures = ['testusers']
57 57
def test_reset_password(self): 58 58 def test_reset_password(self):
# submit the request to reset the password 59 59 # submit the request to reset the password
url = '/api/request_password_reset/' 60 60 url = '/api/request_password_reset/'
post_data = {'email': 'none@none.com'} 61 61 post_data = {'email': 'none@none.com'}
self.client.post(url, post_data, format='json') 62 62 self.client.post(url, post_data, format='json')
self.assertEqual(len(mail.outbox), 1) 63 63 self.assertEqual(len(mail.outbox), 1)
self.assertIn('reset your password', mail.outbox[0].body) 64 64 self.assertIn('reset your password', mail.outbox[0].body)
65 65
# capture the reset token from the email 66 66 # capture the reset token from the email
capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)', 67 67 capture = search('https://flashy.cards/app/resetpassword/(\d+)/(.*)',
mail.outbox[0].body) 68 68 mail.outbox[0].body)
patch_data = {'new_password': '4321'} 69 69 patch_data = {'new_password': '4321'}
patch_data['uid'] = capture.group(1) 70 70 patch_data['uid'] = capture.group(1)
reset_token = capture.group(2) 71 71 reset_token = capture.group(2)
72 72
# try to reset the password with the wrong reset token 73 73 # try to reset the password with the wrong reset token
patch_data['token'] = 'wrong_token' 74 74 patch_data['token'] = 'wrong_token'
url = '/api/reset_password/' 75 75 url = '/api/reset_password/'
response = self.client.post(url, patch_data, format='json') 76 76 response = self.client.post(url, patch_data, format='json')
self.assertContains(response, 'Could not verify reset token', status_code=400) 77 77 self.assertContains(response, 'Could not verify reset token', status_code=400)
78 78
# try to reset the password with the correct token 79 79 # try to reset the password with the correct token
patch_data['token'] = reset_token 80 80 patch_data['token'] = reset_token
response = self.client.post(url, patch_data, format='json') 81 81 response = self.client.post(url, patch_data, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 82 82 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
user = User.objects.get(id=patch_data['uid']) 83 83 user = User.objects.get(id=patch_data['uid'])
assert user.check_password(patch_data['new_password']) 84 84 assert user.check_password(patch_data['new_password'])
85 85
86 86
class RegistrationTest(APITestCase): 87 87 class RegistrationTest(APITestCase):
def test_create_account(self): 88 88 def test_create_account(self):
url = '/api/register/' 89 89 url = '/api/register/'
90 90
# missing password 91 91 # missing password
data = {'email': 'none@none.com'} 92 92 data = {'email': 'none@none.com'}
response = self.client.post(url, data, format='json') 93 93 response = self.client.post(url, data, format='json')
self.assertContains(response, 'password', status_code=400) 94 94 self.assertContains(response, 'password', status_code=400)
95 95
# missing email 96 96 # missing email
data = {'password': '1234'} 97 97 data = {'password': '1234'}
response = self.client.post(url, data, format='json') 98 98 response = self.client.post(url, data, format='json')
self.assertContains(response, 'email', status_code=400) 99 99 self.assertContains(response, 'email', status_code=400)
100 100
# create a user 101 101 # create a user
data = {'email': 'none@none.com', 'password': '1234'} 102 102 data = {'email': 'none@none.com', 'password': '1234'}
response = self.client.post(url, data, format='json') 103 103 response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, HTTP_201_CREATED) 104 104 self.assertEqual(response.status_code, HTTP_201_CREATED)
105 105
# user should not be confirmed 106 106 # user should not be confirmed
user = User.objects.get(email="none@none.com") 107 107 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 108 108 self.assertFalse(user.is_confirmed)
109 109
# check that the confirmation key was sent 110 110 # check that the confirmation key was sent
self.assertEqual(len(mail.outbox), 1) 111 111 self.assertEqual(len(mail.outbox), 1)
self.assertIn(user.confirmation_key, mail.outbox[0].body) 112 112 self.assertIn(user.confirmation_key, mail.outbox[0].body)
113 113
# log the user out 114 114 # log the user out
self.client.logout() 115 115 self.client.logout()
116 116
# log the user in with their registered credentials 117 117 # log the user in with their registered credentials
self.client.login(email='none@none.com', password='1234') 118 118 self.client.login(email='none@none.com', password='1234')
119 119
# try activating with an invalid key 120 120 # try activating with an invalid key
121 121
url = '/api/me/' 122 122 url = '/api/me/'
response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'}) 123 123 response = self.client.patch(url, {'confirmation_key': 'NOT A KEY'})
self.assertContains(response, 'confirmation_key is invalid', status_code=400) 124 124 self.assertContains(response, 'confirmation_key is invalid', status_code=400)
125 125
# try activating with the valid key 126 126 # try activating with the valid key
response = self.client.patch(url, {'confirmation_key': user.confirmation_key}) 127 127 response = self.client.patch(url, {'confirmation_key': user.confirmation_key})
self.assertTrue(response.data['is_confirmed']) 128 128 self.assertTrue(response.data['is_confirmed'])
129 129
130 130
class ProfileViewTest(APITestCase): 131 131 class ProfileViewTest(APITestCase):
fixtures = ['testusers'] 132 132 fixtures = ['testusers']
133 133
def test_get_me(self): 134 134 def test_get_me(self):
url = '/api/me/' 135 135 url = '/api/me/'
response = self.client.get(url, format='json') 136 136 response = self.client.get(url, format='json')
# since we're not logged in, we shouldn't be able to see this 137 137 # since we're not logged in, we shouldn't be able to see this
self.assertEqual(response.status_code, 403) 138 138 self.assertEqual(response.status_code, 403)
139 139
self.client.login(email='none@none.com', password='1234') 140 140 self.client.login(email='none@none.com', password='1234')
response = self.client.get(url, format='json') 141 141 response = self.client.get(url, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 142 142 self.assertEqual(response.status_code, HTTP_200_OK)
143 143
144 144
class UserSectionsTest(APITestCase): 145 145 class UserSectionsTest(APITestCase):
fixtures = ['testusers', 'testsections'] 146 146 fixtures = ['testusers', 'testsections']
147 147
def setUp(self): 148 148 def setUp(self):
self.user = User.objects.get(pk=1) 149 149 self.user = User.objects.get(pk=1)
self.client.login(email='none@none.com', password='1234') 150 150 self.client.login(email='none@none.com', password='1234')
self.section = Section.objects.get(pk=1) 151 151 self.section = Section.objects.get(pk=1)
self.section.enroll(self.user) 152 152 self.section.enroll(self.user)
153 153
def test_get_user_sections(self): 154 154 def test_get_user_sections(self):
response = self.client.get('/api/me/sections/', format='json') 155 155 response = self.client.get('/api/me/sections/', format='json')
self.assertEqual(response.status_code, 200) 156 156 self.assertEqual(response.status_code, 200)
self.assertContains(response, 'Goldstein') 157 157 self.assertContains(response, 'Goldstein')
158 158
159 159
class PasswordChangeTest(APITestCase): 160 160 class PasswordChangeTest(APITestCase):
fixtures = ['testusers'] 161 161 fixtures = ['testusers']
162 162
def test_change_password(self): 163 163 def test_change_password(self):
url = '/api/me/' 164 164 url = '/api/me/'
user = User.objects.get(email='none@none.com') 165 165 user = User.objects.get(email='none@none.com')
self.assertTrue(user.check_password('1234')) 166 166 self.assertTrue(user.check_password('1234'))
167 167
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 168 168 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 169 169 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
170 170
self.client.login(email='none@none.com', password='1234') 171 171 self.client.login(email='none@none.com', password='1234')
response = self.client.patch(url, {'new_password': '4321'}, format='json') 172 172 response = self.client.patch(url, {'new_password': '4321'}, format='json')
self.assertContains(response, 'old_password is required', status_code=400) 173 173 self.assertContains(response, 'old_password is required', status_code=400)
174 174
response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json') 175 175 response = self.client.patch(url, {'new_password': '4321', 'old_password': '4321'}, format='json')
self.assertContains(response, 'old_password is incorrect', status_code=400) 176 176 self.assertContains(response, 'old_password is incorrect', status_code=400)
177 177
response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json') 178 178 response = self.client.patch(url, {'new_password': '4321', 'old_password': '1234'}, format='json')
self.assertEqual(response.status_code, 200) 179 179 self.assertEqual(response.status_code, 200)
user = User.objects.get(email='none@none.com') 180 180 user = User.objects.get(email='none@none.com')
181 181
self.assertFalse(user.check_password('1234')) 182 182 self.assertFalse(user.check_password('1234'))
self.assertTrue(user.check_password('4321')) 183 183 self.assertTrue(user.check_password('4321'))
184 184
185 185
class DeleteUserTest(APITestCase): 186 186 class DeleteUserTest(APITestCase):
fixtures = ['testusers'] 187 187 fixtures = ['testusers']
188 188
def test_delete_user(self): 189 189 def test_delete_user(self):
url = '/api/me/' 190 190 url = '/api/me/'
user = User.objects.get(email='none@none.com') 191 191 user = User.objects.get(email='none@none.com')
192 192
self.client.login(email='none@none.com', password='1234') 193 193 self.client.login(email='none@none.com', password='1234')
self.client.delete(url) 194 194 self.client.delete(url)
self.assertFalse(User.objects.filter(email='none@none.com').exists()) 195 195 self.assertFalse(User.objects.filter(email='none@none.com').exists())
196 196
197 197
class FlashcardDetailTest(APITestCase): 198 198 class FlashcardDetailTest(APITestCase):
fixtures = ['testusers', 'testsections'] 199 199 fixtures = ['testusers', 'testsections']
200 200
def setUp(self): 201 201 def setUp(self):
self.section = Section.objects.get(pk=1) 202 202 self.section = Section.objects.get(pk=1)
self.user = User.objects.get(email='none@none.com') 203 203 self.user = User.objects.get(email='none@none.com')
self.section.enroll(self.user) 204 204 self.section.enroll(self.user)
self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2), 205 205 self.inaccessible_flashcard = Flashcard(text="can't touch this!", section=Section.objects.get(pk=2),
author=self.user) 206 206 author=self.user)
self.inaccessible_flashcard.save() 207 207 self.inaccessible_flashcard.save()
self.flashcard = Flashcard(text="jason", section=self.section, author=self.user) 208 208 self.flashcard = Flashcard(text="jason", section=self.section, author=self.user)
self.flashcard.save() 209 209 self.flashcard.save()
#self.flashcard.add_to_deck(self.user) 210 210 #self.flashcard.add_to_deck(self.user)
self.client.login(email='none@none.com', password='1234') 211 211 self.client.login(email='none@none.com', password='1234')
212 212
def test_edit_flashcard(self): 213 213 def test_edit_flashcard(self):
user = self.user 214 214 user = self.user
flashcard = self.flashcard 215 215 flashcard = self.flashcard
url = "/api/flashcards/{}/".format(flashcard.pk) 216 216 url = "/api/flashcards/{}/".format(flashcard.pk)
data = {'text': 'new wow for the flashcard', 217 217 data = {'text': 'new wow for the flashcard',
'mask': '[[0,4]]'} 218 218 'mask': '[[0,4]]'}
self.assertNotEqual(flashcard.text, data['text']) 219 219 self.assertNotEqual(flashcard.text, data['text'])
response = self.client.patch(url, data, format='json') 220 220 response = self.client.patch(url, data, format='json')
self.assertEqual(response.status_code, HTTP_200_OK) 221 221 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data['text'], data['text']) 222 222 self.assertEqual(response.data['text'], data['text'])
data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2), 223 223 data = {'material_date': datetime.datetime(2015, 4, 12, 2, 2, 2),
'mask': '[[1, 3]]'} 224 224 'mask': '[[1, 3]]'}
user2 = User.objects.create(email='wow@wow.wow', password='wow') 225 225 user2 = User.objects.create(email='wow@wow.wow', password='wow')
user2.sections.add(self.section) 226 226 user2.sections.add(self.section)
user2.save() 227 227 user2.save()
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 228 228 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
response = self.client.patch(url, data, format='json') 229 229 response = self.client.patch(url, data, format='json')
serializer = FlashcardSerializer(data=response.data) 230 230 serializer = FlashcardSerializer(data=response.data)
serializer.is_valid(raise_exception=True) 231 231 serializer.is_valid(raise_exception=True)
self.assertEqual(response.status_code, HTTP_200_OK) 232 232 self.assertEqual(response.status_code, HTTP_200_OK)
# self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date'])) 233 233 # self.assertEqual(serializer.validated_data['material_date'], utc.localize(data['material_date']))
self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]])) 234 234 self.assertEqual(serializer.validated_data['mask'], FlashcardMask([[1, 3]]))
data = {'mask': '[[3,6]]'} 235 235 data = {'mask': '[[3,6]]'}
response = self.client.patch(url, data, format='json') 236 236 response = self.client.patch(url, data, format='json')
user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard) 237 237 user_flashcard = UserFlashcard.objects.get(user=user, flashcard=flashcard)
self.assertEqual(response.status_code, HTTP_200_OK) 238 238 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]])) 239 239 self.assertEqual(user_flashcard.mask, FlashcardMask([[3, 6]]))
240 240
def test_create_flashcard(self): 241 241 def test_create_flashcard(self):
data = {'text': 'this is a flashcard', 242 242 data = {'text': 'this is a flashcard',
'material_date': now(), 243 243 'material_date': now(),
'mask': '[]', 244 244 'mask': '[]',
'section': '1', 245 245 'section': '1',
'previous': None} 246 246 'previous': None}
response = self.client.post("/api/flashcards/", data, format="json") 247 247 response = self.client.post("/api/flashcards/", data, format="json")
self.assertEqual(response.status_code, HTTP_201_CREATED) 248 248 self.assertEqual(response.status_code, HTTP_201_CREATED)
self.assertEqual(response.data['text'], data['text']) 249 249 self.assertEqual(response.data['text'], data['text'])
self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists()) 250 250 self.assertTrue(Flashcard.objects.filter(section__pk=1, text=data['text']).exists())
251 251
def test_get_flashcard(self): 252 252 def test_get_flashcard(self):
response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json") 253 253 response = self.client.get("/api/flashcards/%d/" % self.flashcard.id, format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 254 254 self.assertEqual(response.status_code, HTTP_200_OK)
self.assertEqual(response.data["text"], "jason") 255 255 self.assertEqual(response.data["text"], "jason")
256 256
def test_hide_flashcard(self): 257 257 def test_hide_flashcard(self):
response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json') 258 258 response = self.client.post('/api/flashcards/%d/hide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 259 259 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.flashcard.is_hidden_from(self.user)) 260 260 self.assertTrue(self.flashcard.is_hidden_from(self.user))
261 261
response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json') 262 262 response = self.client.post('/api/flashcards/%d/hide/' % self.inaccessible_flashcard.pk, format='json')
# This should fail because the user is not enrolled in section id 2 263 263 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 264 264 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
265 265
def test_unhide_flashcard(self): 266 266 def test_unhide_flashcard(self):
self.flashcard.hide_from(self.user) 267 267 self.flashcard.hide_from(self.user)
268 268
response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json') 269 269 response = self.client.post('/api/flashcards/%d/unhide/' % self.flashcard.id, format='json')
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 270 270 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
271 271
response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json') 272 272 response = self.client.post('/api/flashcards/%d/unhide/' % self.inaccessible_flashcard.pk, format='json')
273 273
# This should fail because the user is not enrolled in section id 2 274 274 # This should fail because the user is not enrolled in section id 2
self.assertEqual(response.status_code, HTTP_403_FORBIDDEN) 275 275 self.assertEqual(response.status_code, HTTP_403_FORBIDDEN)
276 276
277 277
class SectionViewSetTest(APITestCase): 278 278 class SectionViewSetTest(APITestCase):
fixtures = ['testusers', 'testsections'] 279 279 fixtures = ['testusers', 'testsections']
280 280
def setUp(self): 281 281 def setUp(self):
self.client.login(email='none@none.com', password='1234') 282 282 self.client.login(email='none@none.com', password='1234')
self.user = User.objects.get(email='none@none.com') 283 283 self.user = User.objects.get(email='none@none.com')
self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(), 284 284 self.flashcard = Flashcard(text="jason", section=Section.objects.get(pk=1), material_date=now(),
author=self.user) 285 285 author=self.user)
self.flashcard.save() 286 286 self.flashcard.save()
self.section = Section.objects.get(pk=1) 287 287 self.section = Section.objects.get(pk=1)
288 288
def test_list_sections(self): 289 289 def test_list_sections(self):
response = self.client.get("/api/sections/", format="json") 290 290 response = self.client.get("/api/sections/", format="json")
self.assertEqual(response.status_code, HTTP_200_OK) 291 291 self.assertEqual(response.status_code, HTTP_200_OK)
292 292
def test_section_enroll(self): 293 293 def test_section_enroll(self):
section = self.section 294 294 section = self.section
self.assertFalse(self.user.sections.filter(pk=section.pk)) 295 295 self.assertFalse(self.user.sections.filter(pk=section.pk))
296 296
# test enrolling in a section without a whitelist 297 297 # test enrolling in a section without a whitelist
response = self.client.post('/api/sections/%d/enroll/' % section.pk) 298 298 response = self.client.post('/api/sections/%d/enroll/' % section.pk)
self.assertEqual(response.status_code, HTTP_204_NO_CONTENT) 299 299 self.assertEqual(response.status_code, HTTP_204_NO_CONTENT)
self.assertTrue(self.user.sections.filter(pk=section.pk).exists()) 300 300 self.assertTrue(self.user.sections.filter(pk=section.pk).exists())
301 301
section = Section.objects.get(pk=2) 302 302 section = Section.objects.get(pk=2)
WhitelistedAddress.objects.create(email='bad@none.com', section=section) 303 303 WhitelistedAddress.objects.create(email='bad@none.com', section=section)
304 304
# test enrolling in a section when not on the whitelist 305 305 # test enrolling in a section when not on the whitelist
flashcards/tests/test_models.py View file @ 2f49f82
from datetime import datetime 1 1 from datetime import datetime
2 2
from django.test import TestCase 3 3 from django.test import TestCase
from flashcards.models import User, Section, Flashcard, UserFlashcard 4 4 from flashcards.models import User, Section, Flashcard, UserFlashcard, UserFlashcardQuiz
from flashcards.validators import FlashcardMask, OverlapIntervalException 5 5 from flashcards.validators import FlashcardMask, OverlapIntervalException
6 from flashcards.serializers import QuizRequestSerializer, QuizResponseSerializer, QuizAnswerRequestSerializer
7 from flashy.settings import QUARTER_START, QUARTER_END
6 8
7 9
class RegistrationTests(TestCase): 8 10 class RegistrationTests(TestCase):
def setUp(self): 9 11 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 10 12 User.objects.create_user(email="none@none.com", password="1234")
11 13
def test_email_confirmation(self): 12 14 def test_email_confirmation(self):
user = User.objects.get(email="none@none.com") 13 15 user = User.objects.get(email="none@none.com")
self.assertFalse(user.is_confirmed) 14 16 self.assertFalse(user.is_confirmed)
user.confirm_email(user.confirmation_key) 15 17 user.confirm_email(user.confirmation_key)
self.assertTrue(user.is_confirmed) 16 18 self.assertTrue(user.is_confirmed)
17 19
18 20
class UserTests(TestCase): 19 21 class UserTests(TestCase):
def setUp(self): 20 22 def setUp(self):
User.objects.create_user(email="none@none.com", password="1234") 21 23 User.objects.create_user(email="none@none.com", password="1234")
Section.objects.create(department='dept', 22 24 Section.objects.create(department='dept',
course_num='101a', 23 25 course_num='101a',
course_title='how 2 test', 24 26 course_title='how 2 test',
instructor='George Lucas', 25 27 instructor='George Lucas',
quarter='SP15') 26 28 quarter='SP15')
27 29
def test_section_list(self): 28 30 def test_section_list(self):
section = Section.objects.get(course_num='101a') 29 31 section = Section.objects.get(course_num='101a')
user = User.objects.get(email="none@none.com") 30 32 user = User.objects.get(email="none@none.com")
self.assertNotIn(section, user.sections.all()) 31 33 self.assertNotIn(section, user.sections.all())
user.sections.add(section) 32 34 user.sections.add(section)
self.assertIn(section, user.sections.all()) 33 35 self.assertIn(section, user.sections.all())
user.sections.add(section) 34 36 user.sections.add(section)
self.assertEqual(user.sections.count(), 1) 35 37 self.assertEqual(user.sections.count(), 1)
user.sections.remove(section) 36 38 user.sections.remove(section)
self.assertEqual(user.sections.count(), 0) 37 39 self.assertEqual(user.sections.count(), 0)
38 40
39 41
class FlashcardMaskTest(TestCase): 40 42 class FlashcardMaskTest(TestCase):
def test_empty(self): 41 43 def test_empty(self):
try: 42 44 try:
fm = FlashcardMask([]) 43 45 fm = FlashcardMask([])
self.assertEqual(fm.max_offset(), -1) 44 46 self.assertEqual(fm.max_offset(), -1)
except TypeError: 45 47 except TypeError:
self.fail() 46 48 self.fail()
try: 47 49 try:
fm = FlashcardMask('') 48 50 fm = FlashcardMask('')
self.assertEqual(fm.max_offset(), -1) 49 51 self.assertEqual(fm.max_offset(), -1)
except TypeError: 50 52 except TypeError:
self.fail() 51 53 self.fail()
try: 52 54 try:
fm = FlashcardMask(None) 53 55 fm = FlashcardMask(None)
self.assertEqual(fm.max_offset(), -1) 54 56 self.assertEqual(fm.max_offset(), -1)
except TypeError: 55 57 except TypeError:
self.fail() 56 58 self.fail()
57 59
def test_iterable(self): 58 60 def test_iterable(self):
try: 59 61 try:
FlashcardMask(1) 60 62 FlashcardMask(1)
except TypeError as te: 61 63 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 62 64 self.assertEqual(te.message, "Interval not a valid iterable")
try: 63 65 try:
FlashcardMask([1, 2, 4]) 64 66 FlashcardMask([1, 2, 4])
except TypeError as te: 65 67 except TypeError as te:
self.assertEqual(te.message, "Interval not a valid iterable") 66 68 self.assertEqual(te.message, "Interval not a valid iterable")
67 69
def test_interval(self): 68 70 def test_interval(self):
try: 69 71 try:
FlashcardMask([[1, 2, 3], [1]]) 70 72 FlashcardMask([[1, 2, 3], [1]])
except TypeError as te: 71 73 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 72 74 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 73 75 try:
FlashcardMask([[1, 2], [1, 2, 4]]) 74 76 FlashcardMask([[1, 2], [1, 2, 4]])
except TypeError as te: 75 77 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 76 78 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 77 79 try:
FlashcardMask(([1, 2], [1])) 78 80 FlashcardMask(([1, 2], [1]))
except TypeError as te: 79 81 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 80 82 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
try: 81 83 try:
FlashcardMask("[1,2,3]") 82 84 FlashcardMask("[1,2,3]")
except TypeError as te: 83 85 except TypeError as te:
self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end") 84 86 self.assertEqual(te.message, "Intervals must have exactly 2 elements, begin and end")
85 87
def test_overlap(self): 86 88 def test_overlap(self):
try: 87 89 try:
FlashcardMask({(1, 2), (2, 5)}) 88 90 FlashcardMask({(1, 2), (2, 5)})
except OverlapIntervalException as oie: 89 91 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 90 92 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 91 93 try:
FlashcardMask({(1, 20), (12, 15)}) 92 94 FlashcardMask({(1, 20), (12, 15)})
except OverlapIntervalException as oie: 93 95 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 94 96 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
try: 95 97 try:
FlashcardMask({(2, 1), (5, 2)}) 96 98 FlashcardMask({(2, 1), (5, 2)})
except OverlapIntervalException as oie: 97 99 except OverlapIntervalException as oie:
self.assertEqual(oie.message, "Invalid interval offsets in the mask") 98 100 self.assertEqual(oie.message, "Invalid interval offsets in the mask")
99 101
100 102
101
class FlashcardTests(TestCase): 102 103 class FlashcardTests(TestCase):
def setUp(self): 103 104 def setUp(self):
section = Section.objects.create(department='dept', 104 105 section = Section.objects.create(department='dept',
course_num='101a', 105 106 course_num='101a',
course_title='how 2 test', 106 107 course_title='how 2 test',
instructor='George Lucas', 107 108 instructor='George Lucas',
quarter='SP15') 108 109 quarter='SP15')
user = User.objects.create_user(email="none@none.com", password="1234") 109 110 user = User.objects.create_user(email="none@none.com", password="1234")
user.sections.add(section) 110 111 user.sections.add(section)
flashcard = Flashcard.objects.create(text="This is the text of the Flashcard", 111 112 flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
section=section, 112 113 section=section,
author=user, 113 114 author=user,
material_date=datetime.now(), 114 115 material_date=datetime.now(),
previous=None, 115 116 previous=None,
mask={(24,34), (0, 4)}) 116 117 mask={(24, 34), (0, 4)})
user.save() 117 118 user.save()
section.save() 118 119 section.save()
flashcard.save() 119 120 flashcard.save()
120 121
def test_flashcard_edit(self): 121 122 def test_flashcard_edit(self):
user = User.objects.get(email="none@none.com") 122 123 user = User.objects.get(email="none@none.com")
user2 = User.objects.create_user(email="wow@wow.com", password="wow") 123 124 user2 = User.objects.create_user(email="wow@wow.com", password="wow")
section = Section.objects.get(course_title='how 2 test') 124 125 section = Section.objects.get(course_title='how 2 test')
user2.sections.add(section) 125 126 user2.sections.add(section)
user2.save() 126 127 user2.save()
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 127 128 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
pk_backup = flashcard.pk 128 129 pk_backup = flashcard.pk
self.assertTrue(user.is_in_section(section)) 129 130 self.assertTrue(user.is_in_section(section))
flashcard.edit(user, {}) 130 131 flashcard.edit(user, {})
self.assertIsNotNone(flashcard.pk) 131 132 self.assertIsNotNone(flashcard.pk)
UserFlashcard.objects.create(user=user2, flashcard=flashcard).save() 132 133 UserFlashcard.objects.create(user=user2, flashcard=flashcard).save()
flashcard.edit(user2, {'text': 'This is the new text'}) 133 134 flashcard.edit(user2, {'text': 'This is the new text'})
self.assertNotEqual(flashcard.pk, pk_backup) 134 135 self.assertNotEqual(flashcard.pk, pk_backup)
self.assertEqual(flashcard.text, 'This is the new text') 135 136 self.assertEqual(flashcard.text, 'This is the new text')
136 137
def test_mask_field(self): 137 138 def test_mask_field(self):
user = User.objects.get(email="none@none.com") 138 139 user = User.objects.get(email="none@none.com")
flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard") 139 140 flashcard = Flashcard.objects.filter(author=user).get(text="This is the text of the Flashcard")
self.assertTrue(isinstance(flashcard.mask, set)) 140 141 self.assertTrue(isinstance(flashcard.mask, set))
self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask])) 141 142 self.assertTrue(all([isinstance(interval, tuple) for interval in flashcard.mask]))
blank1, blank2 = sorted(list(flashcard.mask)) 142 143 blank1, blank2 = sorted(list(flashcard.mask))
self.assertEqual(flashcard.text[slice(*blank1)], 'This') 143 144 self.assertEqual(flashcard.text[slice(*blank1)], 'This')
self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard') 144 145 self.assertEqual(flashcard.text[slice(*blank2)], 'Flashcard')
try: 145 146 try:
flashcard.mask = {(10, 34), (0, 14)} 146 147 flashcard.mask = {(10, 34), (0, 14)}
flashcard.save() 147 148 flashcard.save()
self.fail() 148 149 self.fail()
except OverlapIntervalException: 149 150 except OverlapIntervalException:
self.assertTrue(True) 150 151 self.assertTrue(True)
152
153
154 class UserFlashcardQuizTests(TestCase):
155 def setUp(self):
156 self.section = Section.objects.create(department='dept',
157 course_num='101a',
158 course_title='how 2 test',
159 instructor='George Lucas',
160 quarter='SP15')
161 self.user = User.objects.create_user(email="none@none.com", password="1234")
162 self.user.sections.add(self.section)
163 self.flashcard = Flashcard.objects.create(text="This is the text of the Flashcard",
164 section=self.section,
165 author=self.user,
166 material_date=datetime.now(),
167 previous=None,
168 mask=[(24, 33), (0, 4)])
169 self.user.save()
170 self.section.save()
171 self.flashcard.save()
172 self.user_flashcard = UserFlashcard.objects.create(flashcard=self.flashcard,
173 user=self.user,
174 mask=self.flashcard.mask,
175 pulled=datetime.now())
176 self.user_flashcard.save()
177 self.user_flashcard.refresh_from_db()
178 self.flashcard.refresh_from_db()
179
180 def test_quiz_request(self):
181 data = {'sections': [1], 'material_date_begin': QUARTER_START, 'material_date_end': QUARTER_END}
182 serializer = QuizRequestSerializer(user=self.user, data=data)
183 serializer.is_valid(raise_exception=True)
184 user_flashcard_quiz = serializer.create(serializer.validated_data)
185 self.assertTrue(isinstance(user_flashcard_quiz, UserFlashcardQuiz))
186 mask = user_flashcard_quiz.user_flashcard.mask.get_random_blank()
187 self.assertIn(mask, [(24, 33), (0, 4)])
188 user_flashcard_quiz.blanked_word = user_flashcard_quiz.user_flashcard.flashcard.text[slice(*mask)]
189 self.assertIn(user_flashcard_quiz.blanked_word, ["This", "Flashcard"])
190 user_flashcard_quiz.save()
191 response = QuizResponseSerializer(instance=user_flashcard_quiz, mask=mask).data
192 self.assertEqual(response['pk'], 1)
193 self.assertEqual(response['section'], 1)
194 self.assertEqual(response['text'], user_flashcard_quiz.user_flashcard.flashcard.text)
195 self.assertEqual(response['mask'], mask)
196
flashcards/validators.py View file @ 2f49f82
from collections import Iterable 1 1 from collections import Iterable
2 from random import sample
2 3
3 4
class FlashcardMask(set): 4 5 class FlashcardMask(set):
def __init__(self, iterable, *args, **kwargs): 5 6 def __init__(self, iterable, *args, **kwargs):
if iterable is None or iterable == '': 6 7 if iterable is None or iterable == '':
iterable = [] 7 8 iterable = []
self._iterable_check(iterable) 8 9 self._iterable_check(iterable)
iterable = map(tuple, iterable) 9 10 iterable = map(tuple, iterable)
super(FlashcardMask, self).__init__(iterable, *args, **kwargs) 10 11 super(FlashcardMask, self).__init__(iterable, *args, **kwargs)
self._interval_check() 11 12 self._interval_check()
self._overlap_check() 12 13 self._overlap_check()
13 14
def max_offset(self): 14 15 def max_offset(self):
return self._end 15 16 return self._end
17
18 def get_random_blank(self):
19 if self.max_offset() > 0:
20 return sample(self, 1)[0]
21 return ()
16 22
def _iterable_check(self, iterable): 17 23 def _iterable_check(self, iterable):
if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]): 18 24 if not isinstance(iterable, Iterable) or not all([isinstance(i, Iterable) for i in iterable]):
raise TypeError("Interval not a valid iterable") 19 25 raise TypeError("Interval not a valid iterable")
20 26
def _interval_check(self): 21 27 def _interval_check(self):
if not all([len(i) == 2 for i in self]): 22 28 if not all([len(i) == 2 for i in self]):
raise TypeError("Intervals must have exactly 2 elements, begin and end") 23 29 raise TypeError("Intervals must have exactly 2 elements, begin and end")
24 30
def _overlap_check(self): 25 31 def _overlap_check(self):
p_beg, p_end = -1, -1 26 32 p_beg, p_end = -1, -1
for interval in sorted(self): 27 33 for interval in sorted(self):
beg, end = map(int, interval) 28 34 beg, end = map(int, interval)
if not (0 <= beg <= 255) or not (0 <= end <= 255) or not (beg <= end) or not (beg > p_end): 29 35 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") 30 36 raise OverlapIntervalException((beg, end), "Invalid interval offsets in the mask")
p_beg, p_end = beg, end 31 37 p_beg, p_end = beg, end
self._end = p_end 32 38 self._end = p_end
flashcards/views.py View file @ 2f49f82
import django 1 1 import django
2 2
from django.contrib import auth 3 3 from django.contrib import auth
from django.core.cache import cache 4 4 from django.core.cache import cache
from django.shortcuts import get_object_or_404 5 5 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection 6 6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcard, UserFlashcardQuiz
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 8 8 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 9 9 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, DeepSectionSerializer 10 10 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
11 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 11 12 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 12 13 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin 13 14 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 14 15 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 15 16 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 16 17 from django.core.mail import send_mail
from django.contrib.auth import authenticate 17 18 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 18 19 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 19 20 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 20 21 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 21 22 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 22 23 from simple_email_confirmation import EmailAddress
24 from random import sample
23 25
24 26
class SectionViewSet(ReadOnlyModelViewSet): 25 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 26 28 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 27 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 28 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 29 31 permission_classes = [IsAuthenticated]
30 32
@detail_route(methods=['GET']) 31 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 32 34 def flashcards(self, request, pk):
""" 33 35 """
Gets flashcards for a section, excluding hidden cards. 34 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 35 37 Returned in strictly chronological order (material date).
""" 36 38 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 37 39 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 38 40 return Response(FlashcardSerializer(flashcards, many=True).data)
39 41
@detail_route(methods=['post']) 40 42 @detail_route(methods=['POST'])
def enroll(self, request, pk): 41 43 def enroll(self, request, pk):
""" 42 44 """
Add the current user to a specified section 43 45 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. 44 46 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 45 47 ---
view_mocker: flashcards.api.mock_no_params 46 48 view_mocker: flashcards.api.mock_no_params
""" 47 49 """
48 50
self.get_object().enroll(request.user) 49 51 self.get_object().enroll(request.user)
return Response(status=HTTP_204_NO_CONTENT) 50 52 return Response(status=HTTP_204_NO_CONTENT)
51 53
@detail_route(methods=['post']) 52 54 @detail_route(methods=['POST'])
def drop(self, request, pk): 53 55 def drop(self, request, pk):
""" 54 56 """
Remove the current user from a specified section 55 57 Remove the current user from a specified section
If the user is not in the class, the request will fail. 56 58 If the user is not in the class, the request will fail.
--- 57 59 ---
view_mocker: flashcards.api.mock_no_params 58 60 view_mocker: flashcards.api.mock_no_params
""" 59 61 """
try: 60 62 try:
self.get_object().drop(request.user) 61 63 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 62 64 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 63 65 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 64 66 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 65 67 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 66 68 return Response(status=HTTP_204_NO_CONTENT)
67 69
@list_route(methods=['GET']) 68 70 @list_route(methods=['GET'])
def search(self, request): 69 71 def search(self, request):
""" 70 72 """
Returns a list of sections which match a user's query 71 73 Returns a list of sections which match a user's query
--- 72 74 ---
parameters: 73 75 parameters:
- name: q 74 76 - name: q
description: space-separated list of terms 75 77 description: space-separated list of terms
required: true 76 78 required: true
type: form 77 79 type: form
response_serializer: SectionSerializer 78 80 response_serializer: SectionSerializer
""" 79 81 """
query = request.GET.get('q', None) 80 82 query = request.GET.get('q', None)
if not query: return Response('[]') 81 83 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 82 84 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 83 85 data = SectionSerializer(qs, many=True).data
return Response(data) 84 86 return Response(data)
85 87
@detail_route(methods=['GET']) 86 88 @detail_route(methods=['GET'])
def deck(self, request, pk): 87 89 def deck(self, request, pk):
""" 88 90 """
Gets the contents of a user's deck for a given section. 89 91 Gets the contents of a user's deck for a given section.
""" 90 92 """
qs = request.user.get_deck(self.get_object()) 91 93 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 92 94 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 93 95 return Response(serializer.data)
94 96
@detail_route(methods=['get'], permission_classes=[IsAuthenticated]) 95 97 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 96 98 def ordered_deck(self, request, pk):
""" 97 99 """
Get a chronological order by material_date of flashcards for a section. 98 100 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 99 101 This excludes hidden card.
""" 100 102 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 101 103 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 102 104 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 103 105 return Response(serializer.data)
104 106
@detail_route(methods=['GET']) 105 107 @detail_route(methods=['GET'])
def feed(self, request, pk): 106 108 def feed(self, request, pk):
""" 107 109 """
Gets the contents of a user's feed for a section. 108 110 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 109 111 Exclude cards that are already in the user's deck
""" 110 112 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 111 113 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 112 114 return Response(serializer.data)
113 115
114 116
class UserSectionListView(ListAPIView): 115 117 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 116 118 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 117 119 permission_classes = [IsAuthenticated]
118 120
def get_queryset(self): 119 121 def get_queryset(self):
return self.request.user.sections.all() 120 122 return self.request.user.sections.all()
121 123
def paginate_queryset(self, queryset): return None 122 124 def paginate_queryset(self, queryset): return None
123 125
124 126
class UserDetail(GenericAPIView): 125 127 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 126 128 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 127 129 permission_classes = [IsAuthenticated]
128 130
def patch(self, request, format=None): 129 131 def patch(self, request, format=None):
""" 130 132 """
Updates the user's password, or verifies their email address 131 133 Updates the user's password, or verifies their email address
--- 132 134 ---
request_serializer: UserUpdateSerializer 133 135 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 134 136 response_serializer: UserSerializer
""" 135 137 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 136 138 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 137 139 data.is_valid(raise_exception=True)
data = data.validated_data 138 140 data = data.validated_data
139 141
if 'new_password' in data: 140 142 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 141 143 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 142 144 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 143 145 request.user.set_password(data['new_password'])
request.user.save() 144 146 request.user.save()
145 147
if 'confirmation_key' in data: 146 148 if 'confirmation_key' in data:
try: 147 149 try:
request.user.confirm_email(data['confirmation_key']) 148 150 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 149 151 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 150 152 raise ValidationError('confirmation_key is invalid')
151 153
return Response(UserSerializer(request.user).data) 152 154 return Response(UserSerializer(request.user).data)
153 155
def get(self, request, format=None): 154 156 def get(self, request, format=None):
""" 155 157 """
Return data about the user 156 158 Return data about the user
--- 157 159 ---
response_serializer: UserSerializer 158 160 response_serializer: UserSerializer
""" 159 161 """
serializer = UserSerializer(request.user, context={'request': request}) 160 162 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 161 163 return Response(serializer.data)
162 164
def delete(self, request): 163 165 def delete(self, request):
""" 164 166 """
Irrevocably delete the user and their data 165 167 Irrevocably delete the user and their data
166 168
Yes, really 167 169 Yes, really
""" 168 170 """
request.user.delete() 169 171 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 170 172 return Response(status=HTTP_204_NO_CONTENT)
171 173
172 174
@api_view(['POST']) 173 175 @api_view(['POST'])
def register(request, format=None): 174 176 def register(request, format=None):
""" 175 177 """
Register a new user 176 178 Register a new user
--- 177 179 ---
request_serializer: EmailPasswordSerializer 178 180 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 179 181 response_serializer: UserSerializer
""" 180 182 """
data = RegistrationSerializer(data=request.data) 181 183 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 182 184 data.is_valid(raise_exception=True)
183 185
User.objects.create_user(**data.validated_data) 184 186 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 185 187 user = authenticate(**data.validated_data)
auth.login(request, user) 186 188 auth.login(request, user)
187 189
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 188 190 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
189 191
190 192
@api_view(['POST']) 191 193 @api_view(['POST'])
def login(request): 192 194 def login(request):
""" 193 195 """
Authenticates user and returns user data if valid. 194 196 Authenticates user and returns user data if valid.
--- 195 197 ---
request_serializer: EmailPasswordSerializer 196 198 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 197 199 response_serializer: UserSerializer
""" 198 200 """
199 201
data = EmailPasswordSerializer(data=request.data) 200 202 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 201 203 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 202 204 user = authenticate(**data.validated_data)
203 205
if user is None: 204 206 if user is None:
raise AuthenticationFailed('Invalid email or password') 205 207 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 206 208 if not user.is_active:
raise NotAuthenticated('Account is disabled') 207 209 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 208 210 auth.login(request, user)
return Response(UserSerializer(request.user).data) 209 211 return Response(UserSerializer(request.user).data)
210 212
211 213
@api_view(['POST']) 212 214 @api_view(['POST'])
@permission_classes((IsAuthenticated, )) 213 215 @permission_classes((IsAuthenticated, ))
def logout(request, format=None): 214 216 def logout(request, format=None):
""" 215 217 """
Logs the authenticated user out. 216 218 Logs the authenticated user out.
""" 217 219 """
auth.logout(request) 218 220 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 219 221 return Response(status=HTTP_204_NO_CONTENT)
220 222
221 223
@api_view(['POST']) 222 224 @api_view(['POST'])
def request_password_reset(request, format=None): 223 225 def request_password_reset(request, format=None):
""" 224 226 """
Send a password reset token/link to the provided email. 225 227 Send a password reset token/link to the provided email.
--- 226 228 ---
request_serializer: PasswordResetRequestSerializer 227 229 request_serializer: PasswordResetRequestSerializer
""" 228 230 """
data = PasswordResetRequestSerializer(data=request.data) 229 231 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 230 232 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 231 233 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 232 234 return Response(status=HTTP_204_NO_CONTENT)
233 235
234 236
@api_view(['POST']) 235 237 @api_view(['POST'])
def reset_password(request, format=None): 236 238 def reset_password(request, format=None):
""" 237 239 """
Updates user's password to new password if token is valid. 238 240 Updates user's password to new password if token is valid.
--- 239 241 ---
request_serializer: PasswordResetSerializer 240 242 request_serializer: PasswordResetSerializer
""" 241 243 """
data = PasswordResetSerializer(data=request.data) 242 244 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 243 245 data.is_valid(raise_exception=True)
244 246
user = User.objects.get(id=data['uid'].value) 245 247 user = User.objects.get(id=data['uid'].value)
# Check token validity. 246 248 # Check token validity.
247 249
if default_token_generator.check_token(user, data['token'].value): 248 250 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 249 251 user.set_password(data['new_password'].value)
user.save() 250 252 user.save()
else: 251 253 else:
raise ValidationError('Could not verify reset token') 252 254 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 253 255 return Response(status=HTTP_204_NO_CONTENT)
254 256
255 257
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 256 258 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 257 259 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 258 260 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 259 261 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
260 262
# Override create in CreateModelMixin 261 263 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 262 264 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 263 265 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 264 266 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 265 267 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 266 268 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 267 269 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 268 270 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 269 271 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 270 272 self.perform_create(flashcard)
headers = self.get_success_headers(data) 271 273 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard) 272 274 response_data = FlashcardSerializer(flashcard)
return Response(response_data.data, status=HTTP_201_CREATED, headers=headers) 273 275 return Response(response_data.data, status=HTTP_201_CREATED, headers=headers)
274 276
275 277
@detail_route(methods=['post']) 276 278 @detail_route(methods=['POST'])
def unhide(self, request, pk): 277 279 def unhide(self, request, pk):
""" 278 280 """
Unhide the given card 279 281 Unhide the given card
--- 280 282 ---
view_mocker: flashcards.api.mock_no_params 281 283 view_mocker: flashcards.api.mock_no_params
""" 282 284 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 283 285 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 284 286 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 285 287 return Response(status=HTTP_204_NO_CONTENT)
286 288
@detail_route(methods=['post']) 287 289 @detail_route(methods=['POST'])
def report(self, request, pk): 288 290 def report(self, request, pk):
""" 289 291 """
Hide the given card 290 292 Hide the given card
--- 291 293 ---
view_mocker: flashcards.api.mock_no_params 292 294 view_mocker: flashcards.api.mock_no_params
""" 293 295 """
self.get_object().report(request.user) 294 296 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 295 297 return Response(status=HTTP_204_NO_CONTENT)
296 298
hide = report 297 299 hide = report
298 300
@detail_route(methods=['POST']) 299 301 @detail_route(methods=['POST'])
def pull(self, request, pk): 300 302 def pull(self, request, pk):
""" 301 303 """
Pull a card from the live feed into the user's deck. 302 304 Pull a card from the live feed into the user's deck.
--- 303 305 ---
view_mocker: flashcards.api.mock_no_params 304 306 view_mocker: flashcards.api.mock_no_params
""" 305 307 """
flashcard = self.get_object() 306 308 flashcard = self.get_object()
user.unpull(flashcard) 307 309 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 308 310 return Response(status=HTTP_204_NO_CONTENT)
309 311
310 312
@detail_route(methods=['POST']) 311 313 @detail_route(methods=['POST'])
def unpull(self, request, pk): 312 314 def unpull(self, request, pk):
""" 313 315 """
Unpull a card from the user's deck 314 316 Unpull a card from the user's deck
--- 315 317 ---
view_mocker: flashcards.api.mock_no_params 316 318 view_mocker: flashcards.api.mock_no_params
""" 317 319 """
user = request.user 318 320 user = request.user
flashcard = self.get_object() 319 321 flashcard = self.get_object()
user.unpull(flashcard) 320 322 user.unpull(flashcard)
return Response(status=HTTP_204_NO_CONTENT) 321 323 return Response(status=HTTP_204_NO_CONTENT)
322 324
def partial_update(self, request, *args, **kwargs): 323 325 def partial_update(self, request, *args, **kwargs):
""" 324 326 """
Edit settings related to a card for the user. 325 327 Edit settings related to a card for the user.
--- 326 328 ---
request_serializer: FlashcardUpdateSerializer 327 329 request_serializer: FlashcardUpdateSerializer
flashy/settings.py View file @ 2f49f82
# Build paths inside the project like this: os.path.join(BASE_DIR, ...) 1 1 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os 2 2 import os
3 from datetime import datetime
4 from pytz import UTC
3 5
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 6 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
5 7
IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ 6 8 IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ
7 9
DEBUG = not IN_PRODUCTION 8 10 DEBUG = not IN_PRODUCTION
9 11
ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] 10 12 ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
11 13
AUTH_USER_MODEL = 'flashcards.User' 12 14 AUTH_USER_MODEL = 'flashcards.User'
REST_FRAMEWORK = { 13 15 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 14 16 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20 15 17 'PAGE_SIZE': 20
} 16 18 }
INSTALLED_APPS = [ 17 19 INSTALLED_APPS = [
'simple_email_confirmation', 18 20 'simple_email_confirmation',
'flashcards', 19 21 'flashcards',
'django.contrib.admin', 20 22 'django.contrib.admin',
'django.contrib.admindocs', 21 23 'django.contrib.admindocs',
'django.contrib.auth', 22 24 'django.contrib.auth',
'django.contrib.contenttypes', 23 25 'django.contrib.contenttypes',
'django.contrib.sessions', 24 26 'django.contrib.sessions',
'django.contrib.messages', 25 27 'django.contrib.messages',
'django.contrib.staticfiles', 26 28 'django.contrib.staticfiles',
27 29 'ws4redis',
'rest_framework_swagger', 28 30 'rest_framework_swagger',
'rest_framework', 29 31 'rest_framework',
] 30 32 ]
31 33
34 WEBSOCKET_URL = '/ws/'
35
36
MIDDLEWARE_CLASSES = ( 32 37 MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 33 38 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 34 39 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 35 40 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 36 41 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 37 42 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 38 43 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 39 44 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 40 45 'django.middleware.security.SecurityMiddleware',
) 41 46 )
42 47
ROOT_URLCONF = 'flashy.urls' 43 48 ROOT_URLCONF = 'flashy.urls'
44 49
AUTHENTICATION_BACKENDS = ( 45 50 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 46 51 'django.contrib.auth.backends.ModelBackend',
) 47 52 )
48 53
TEMPLATES = [ 49 54 TEMPLATES = [
{ 50 55 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 51 56 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 52 57 'DIRS': ['templates/'],
'APP_DIRS': True, 53 58 'APP_DIRS': True,
'OPTIONS': { 54 59 'OPTIONS': {
'context_processors': [ 55 60 'context_processors': [
'django.template.context_processors.debug', 56 61 'django.template.context_processors.debug',
'django.template.context_processors.request', 57 62 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 58 63 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 59 64 'django.contrib.messages.context_processors.messages',
65 'django.core.context_processors.static',
66 'ws4redis.context_processors.default',
], 60 67 ],
}, 61 68 },
}, 62 69 },
] 63 70 ]
64 71
WSGI_APPLICATION = 'flashy.wsgi.application' 65 72 WSGI_APPLICATION = 'ws4redis.django_runserver.application'
66 73
DATABASES = { 67 74 DATABASES = {
'default': { 68 75 'default': {
'ENGINE': 'django.db.backends.sqlite3', 69 76 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 70 77 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 71 78 }
} 72 79 }
73 80
if IN_PRODUCTION: 74 81 if IN_PRODUCTION:
DATABASES['default'] = { 75 82 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 76 83 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 77 84 'NAME': 'flashy',
'USER': 'flashy', 78 85 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 79 86 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 80 87 'HOST': 'localhost',
'PORT': '', 81 88 'PORT': '',
} 82 89 }
83 90
LANGUAGE_CODE = 'en-us' 84 91 LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles' 85 92 TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True 86 93 USE_I18N = True
USE_L10N = True 87 94 USE_L10N = True
USE_TZ = True 88 95 USE_TZ = True
96
97 QUARTER_START = UTC.localize(datetime(2015, 3, 30))
98 QUARTER_END = UTC.localize(datetime(2015, 6, 12))
89 99
STATIC_URL = '/static/' 90 100 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 91 101 STATIC_ROOT = 'static'
92 102
# Four settings just to be sure 93 103 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 94 104 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 95 105 EMAIL_HOST_USER = 'noreply@flashy.cards'
DEFAULT_FROM_EMAIL = 'noreply@flashy.cards' 96 106 DEFAULT_FROM_EMAIL = 'noreply@flashy.cards'
SERVER_EMAIL = 'noreply@flashy.cards' 97 107 SERVER_EMAIL = 'noreply@flashy.cards'
98 108
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 99 109 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
100 110
if IN_PRODUCTION: 101 111 if IN_PRODUCTION:
INSTALLED_APPS.append('django_ses') 102 112 INSTALLED_APPS.append('django_ses')
AWS_SES_REGION_NAME = 'us-west-2' 103 113 AWS_SES_REGION_NAME = 'us-west-2'
AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com' 104 114 AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com'
EMAIL_BACKEND = 'django_ses.SESBackend' 105 115 EMAIL_BACKEND = 'django_ses.SESBackend'
106 116
if IN_PRODUCTION: 107 117 if IN_PRODUCTION:
SESSION_COOKIE_SECURE = True 108 118 SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True 109 119 CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 110 120 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# are we secure yet? 111 121 # are we secure yet?
112 122
if IN_PRODUCTION: 113 123 if IN_PRODUCTION:
LOGGING = { 114 124 LOGGING = {
'version': 1, 115 125 'version': 1,
'disable_existing_loggers': False, 116 126 'disable_existing_loggers': False,
'handlers': { 117 127 'handlers': {
'file': { 118 128 'file': {
'level': 'DEBUG', 119 129 'level': 'DEBUG',
'class': 'logging.FileHandler', 120 130 'class': 'logging.FileHandler',
'filename': 'debug.log', 121 131 'filename': 'debug.log',
}, 122 132 },
flashy/urls.py View file @ 2f49f82
from django.conf.urls import include, url 1 1 from django.conf.urls import include, url
from django.contrib import admin 2 2 from django.contrib import admin
from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \ 3 3 from flashcards.views import SectionViewSet, UserDetail, FlashcardViewSet, UserSectionListView, request_password_reset, \
reset_password, logout, login, register 4 4 reset_password, logout, login, register, UserFlashcardQuizViewSet
from flashy.frontend_serve import serve_with_default 5 5 from flashy.frontend_serve import serve_with_default
from flashy.settings import DEBUG, IN_PRODUCTION 6 6 from flashy.settings import DEBUG, IN_PRODUCTION
from rest_framework.routers import DefaultRouter 7 7 from rest_framework.routers import DefaultRouter
from flashcards.api import * 8 8 from flashcards.api import *
9 9
router = DefaultRouter() 10 10 router = DefaultRouter()
router.register(r'sections', SectionViewSet) 11 11 router.register(r'sections', SectionViewSet)
router.register(r'flashcards', FlashcardViewSet) 12 12 router.register(r'flashcards', FlashcardViewSet)
13 router.register(r'study', UserFlashcardQuizViewSet)
13 14
urlpatterns = [ 14 15 urlpatterns = [
url(r'^api/docs/', include('rest_framework_swagger.urls')), 15 16 url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api/me/$', UserDetail.as_view()), 16 17 url(r'^api/me/$', UserDetail.as_view()),
url(r'^api/register/', register), 17 18 url(r'^api/register/', register),
url(r'^api/login/$', login), 18 19 url(r'^api/login/$', login),
url(r'^api/logout/$', logout), 19 20 url(r'^api/logout/$', logout),
url(r'^api/me/sections/', UserSectionListView.as_view()), 20 21 url(r'^api/me/sections/', UserSectionListView.as_view()),
url(r'^api/request_password_reset/', request_password_reset), 21 22 url(r'^api/request_password_reset/', request_password_reset),
url(r'^api/reset_password/', reset_password), 22 23 url(r'^api/reset_password/', reset_password),
url(r'^api/', include(router.urls)), 23 24 url(r'^api/', include(router.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 24 25 url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)), 25 26 url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 26 27 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
] 27 28 ]
28 29
if IN_PRODUCTION: 29 30 if IN_PRODUCTION:
urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) 30 31 urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),)
31 32
requirements.txt View file @ 2f49f82
#beautifulsoup4 1 1 #beautifulsoup4
Django>=1.8 2 2 Django>=1.8
#django-websocket-redis==0.4.3 3 3 django-websocket-redis
#gevent==1.0.1 4 4 #gevent==1.0.1
#greenlet==0.4.5 5 5 #greenlet==0.4.5
#redis==2.10.3 6 6 #redis==2.10.3
six==1.9.0 7 7 six==1.9.0
djangorestframework 8 8 djangorestframework
docutils 9 9 docutils
django-simple-email-confirmation 10 10 django-simple-email-confirmation
coverage 11 11 coverage
django-rest-swagger 12 12 django-rest-swagger
pytz 13 13 pytz
14 14
scripts/run_production.sh View file @ 2f49f82
#!/bin/bash -xe 1 1 #!/bin/bash -xe
source secrets.sh 2 2 source secrets.sh
source venv/bin/activate 3 3 source venv/bin/activate
newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi 4 4 # newrelic-admin run-program /srv/flashy-backend/venv/bin/gunicorn --pid /run/flashy/gunicorn.pid -w 6 -n flashy -b 127.0.0.1:7002 flashy.wsgi
5 newrelic-admin run-program uwsgi /etc/uwsgi/flashy.ini --touch-reload=/etc/uwsgi/flashy.ini
5 6