Commit f6900af2ccb7324eea6c1c2c211517fa4dfa86ed

Authored by Rohan Rangray
1 parent 2144619015
Exists in master

Added things to collect registration tokens and notify users

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

flashcards/management/commands/notifyusers.py View file @ f6900af
File was created 1 from django.core.management import BaseCommand
2 from flashcards.models import UserFlashcard, Now
3 from django.utils.timezone import now
4 from datetime import timedelta
5
6
7 class Command(BaseCommand):
8 help = 'Notify the users if they have cards to be reviewed'
9
10 def handle(self, *args, **options):
11 notify_list = UserFlashcard.objects.filter(
12 next_review__lte=Now()
13 ).exclude(
14 user__registration_token=None,
15 user__last_notified__range=(now()-timedelta(days=1), now())
flashcards/migrations/0015_user_registration_token.py View file @ f6900af
File was created 1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('flashcards', '0014_auto_20150601_1315'),
11 ]
12
13 operations = [
14 migrations.AddField(
flashcards/migrations/0016_user_last_notified.py View file @ f6900af
File was created 1 # -*- coding: utf-8 -*-
2 from __future__ import unicode_literals
3
4 from django.db import models, migrations
5
6
7 class Migration(migrations.Migration):
8
9 dependencies = [
10 ('flashcards', '0015_user_registration_token'),
11 ]
12
13 operations = [
14 migrations.AddField(
flashcards/models.py View file @ f6900af
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
from datetime import datetime, timedelta 3 3 from datetime import datetime, timedelta
4 from gcm import GCM
4 5
from django.contrib.auth.models import AbstractUser, UserManager 5 6 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 6 7 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 7 8 from django.core.cache import cache
from django.core.exceptions import ValidationError 8 9 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 9 10 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 10 11 from django.core.mail import send_mail
from django.core.validators import MinLengthValidator 11 12 from django.core.validators import MinLengthValidator
from django.db import IntegrityError 12 13 from django.db import IntegrityError
from django.db.models import * 13 14 from django.db.models import *
from django.utils.timezone import now, make_aware 14 15 from django.utils.timezone import now, make_aware
from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT 15 16 from flashy.settings import QUARTER_START, ABSOLUTE_URL_ROOT, IN_PRODUCTION, GCM_API_KEY
from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress 16 17 from simple_email_confirmation import SimpleEmailConfirmationUserMixin, EmailAddress
from fields import MaskField 17 18 from fields import MaskField
from cached_property import cached_property 18 19 from cached_property import cached_property
from flashy.settings import IN_PRODUCTION 19
20 20
21 21
22 22
23 23
24 24
25 25
26 26
27 27
# Hack to fix AbstractUser before subclassing it 28 28 # Hack to fix AbstractUser before subclassing it
29 29
AbstractUser._meta.get_field('email')._unique = True 30 30 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 31 31 AbstractUser._meta.get_field('username')._unique = False
32 32
33 33
class EmailOnlyUserManager(UserManager): 34 34 class EmailOnlyUserManager(UserManager):
""" 35 35 """
A tiny extension of Django's UserManager which correctly creates users 36 36 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 37 37 without usernames (using emails instead).
""" 38 38 """
39 39
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 40 40 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 41 41 """
Creates and saves a User with the given email and password. 42 42 Creates and saves a User with the given email and password.
""" 43 43 """
email = self.normalize_email(email) 44 44 email = self.normalize_email(email)
user = self.model(email=email, 45 45 user = self.model(email=email,
is_staff=is_staff, is_active=True, 46 46 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 47 47 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 48 48 date_joined=now(), **extra_fields)
user.set_password(password) 49 49 user.set_password(password)
user.save(using=self._db) 50 50 user.save(using=self._db)
user.send_confirmation_email() 51 51 user.send_confirmation_email()
return user 52 52 return user
53 53
def create_user(self, email, password=None, **extra_fields): 54 54 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 55 55 user = self._create_user(email, password, False, False, **extra_fields)
56 56
return user 57 57 return user
58 58
def create_superuser(self, email, password, **extra_fields): 59 59 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 60 60 return self._create_user(email, password, True, True, **extra_fields)
61 61
62 62
class FlashcardAlreadyPulledException(Exception): 63 63 class FlashcardAlreadyPulledException(Exception):
pass 64 64 pass
65 65
66 66
class FlashcardNotInDeckException(Exception): 67 67 class FlashcardNotInDeckException(Exception):
pass 68 68 pass
69 69
70 70
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 71 71 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 72 72 """
An extension of Django's default user model. 73 73 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 74 74 We use email as the username field, and include enrolled sections here
""" 75 75 """
objects = EmailOnlyUserManager() 76 76 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 77 77 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 78 78 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 79 79 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 80 80 confirmed_email = BooleanField(default=False)
81 registration_token = CharField(null=True, default=None, max_length=4096)
82 last_notified = DateTimeField(null=True, default=None)
81 83
@property 82 84 @property
def locked(self): 83 85 def locked(self):
if self.confirmed_email: return False 84 86 if self.confirmed_email: return False
return (now() - self.date_joined).days > 0 85 87 return (now() - self.date_joined).days > 0
86 88
def send_confirmation_email(self): 87 89 def send_confirmation_email(self):
body = ''' 88 90 body = '''
Visit the following link to confirm your email address: 89 91 Visit the following link to confirm your email address:
%sapp/verifyemail/%s 90 92 %sapp/verifyemail/%s
91 93
If you did not register for Flashy, no action is required. 92 94 If you did not register for Flashy, no action is required.
''' 93 95 '''
send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key), 94 96 send_mail("Flashy email verification", body % (ABSOLUTE_URL_ROOT, self.confirmation_key),
"noreply@flashy.cards", [self.email]) 95 97 "noreply@flashy.cards", [self.email])
98
99 def notify(self):
100 gcm = GCM(GCM_API_KEY)
101 gcm.plaintext_request(
102 registration_id=self.registration_token,
103 data="You have flashcards to study!"
104 )
105 self.last_notified = now()
106 self.save()
107
108 def set_registration_token(self, token):
109 self.registration_token = token
110 self.save()
96 111
def is_in_section(self, section): 97 112 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 98 113 return self.sections.filter(pk=section.pk).exists()
99 114
def pull(self, flashcard): 100 115 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 101 116 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 102 117 raise ValueError("User not in the section this flashcard belongs to")
103 118
try: 104 119 try:
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 105 120 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
except IntegrityError: 106 121 except IntegrityError:
raise FlashcardAlreadyPulledException() 107 122 raise FlashcardAlreadyPulledException()
user_card.save() 108 123 user_card.save()
109 124
import flashcards.notifications 110 125 import flashcards.notifications
111 126
flashcards.notifications.notify_score_change(flashcard) 112 127 flashcards.notifications.notify_score_change(flashcard)
flashcards.notifications.notify_pull(flashcard, self) 113 128 flashcards.notifications.notify_pull(flashcard, self)
114 129
def unpull(self, flashcard): 115 130 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 116 131 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 117 132 raise ValueError("User not in the section this flashcard belongs to")
118 133
try: 119 134 try:
import flashcards.notifications 120 135 import flashcards.notifications
121 136
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 122 137 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 123 138 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 124 139 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 125 140 except UserFlashcard.DoesNotExist:
raise FlashcardNotInDeckException() 126 141 raise FlashcardNotInDeckException()
127 142
def get_deck(self, section): 128 143 def get_deck(self, section):
if not self.is_in_section(section): 129 144 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 130 145 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 131 146 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
132 147
def request_password_reset(self): 133 148 def request_password_reset(self):
token = default_token_generator.make_token(self) 134 149 token = default_token_generator.make_token(self)
135 150
body = ''' 136 151 body = '''
Visit the following link to reset your password: 137 152 Visit the following link to reset your password:
%sapp/resetpassword/%d/%s 138 153 %sapp/resetpassword/%d/%s
139 154
If you did not request a password reset, no action is required. 140 155 If you did not request a password reset, no action is required.
''' 141 156 '''
142 157
send_mail("Flashy password reset", 143 158 send_mail("Flashy password reset",
body % (ABSOLUTE_URL_ROOT, self.pk, token), 144 159 body % (ABSOLUTE_URL_ROOT, self.pk, token),
"noreply@flashy.cards", 145 160 "noreply@flashy.cards",
[self.email]) 146 161 [self.email])
147 162
@classmethod 148 163 @classmethod
def confirm_email(cls, confirmation_key): 149 164 def confirm_email(cls, confirmation_key):
# This will raise an exception if the email address is invalid 150 165 # This will raise an exception if the email address is invalid
address = EmailAddress.objects.confirm(confirmation_key, save=True).email 151 166 address = EmailAddress.objects.confirm(confirmation_key, save=True).email
user = cls.objects.get(email=address) 152 167 user = cls.objects.get(email=address)
user.confirmed_email = True 153 168 user.confirmed_email = True
user.save() 154 169 user.save()
return address 155 170 return address
156 171
def by_retention(self, sections, material_date_begin, material_date_end): 157 172 def by_retention(self, sections, material_date_begin, material_date_end):
section_pks = sections.values_list('pk') 158 173 section_pks = sections.values_list('pk')
user_flashcard_filter = UserFlashcard.objects.filter( 159 174 user_flashcard_filter = UserFlashcard.objects.filter(
user=self, flashcard__section__pk__in=section_pks, 160 175 user=self, flashcard__section__pk__in=section_pks,
flashcard__material_date__gte=material_date_begin, 161 176 flashcard__material_date__gte=material_date_begin,
flashcard__material_date__lte=material_date_end 162 177 flashcard__material_date__lte=material_date_end
) 163 178 )
164 179
if not user_flashcard_filter.exists(): 165 180 if not user_flashcard_filter.exists():
raise ValidationError("No matching flashcard found in your decks") 166 181 raise ValidationError("No matching flashcard found in your decks")
167 182
return user_flashcard_filter.prefetch_related('userflashcardquiz_set').annotate( 168 183 return user_flashcard_filter.prefetch_related('userflashcardquiz_set').annotate(
study_count=Count('pk'), 169 184 study_count=Count('pk'),
).order_by('next_review') 170 185 ).order_by('next_review')
171 186
172 187
class UserFlashcard(Model): 173 188 class UserFlashcard(Model):
""" 174 189 """
Represents the relationship between a user and a flashcard by: 175 190 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 176 191 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 177 192 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 178 193 3. A user has a flashcard hidden from them
""" 179 194 """
user = ForeignKey('User') 180 195 user = ForeignKey('User')
mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card") 181 196 mask = MaskField(null=True, blank=True, default=None, help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 182 197 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 183 198 flashcard = ForeignKey('Flashcard')
next_review = DateTimeField(null=True) 184 199 next_review = DateTimeField(null=True)
last_interval = IntegerField(default=1) 185 200 last_interval = IntegerField(default=1)
last_response_factor = FloatField(default=2.5) 186 201 last_response_factor = FloatField(default=2.5)
187 202
q_dict = {(False, False): 0, (False, True): 1, (False, None): 2, 188 203 q_dict = {(False, False): 0, (False, True): 1, (False, None): 2,
(True, False): 3, (True, None): 4, (True, True): 5} 189 204 (True, False): 3, (True, None): 4, (True, True): 5}
190 205
def get_mask(self): 191 206 def get_mask(self):
if self.mask is None: 192 207 if self.mask is None:
return self.flashcard.mask 193 208 return self.flashcard.mask
return self.mask 194 209 return self.mask
195 210
def save(self, force_insert=False, force_update=False, using=None, 196 211 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 197 212 update_fields=None):
if self.pk is None: 198 213 if self.pk is None:
self.next_review = datetime.now() + timedelta(days=1) 199 214 self.next_review = datetime.now() + timedelta(days=1)
super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update, 200 215 super(UserFlashcard, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 201 216 using=using, update_fields=update_fields)
202 217
def review(self, user_flashcard_quiz): 203 218 def review(self, user_flashcard_quiz):
q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct] 204 219 q = self.q_dict[(user_flashcard_quiz.response == user_flashcard_quiz.blanked_word), user_flashcard_quiz.correct]
if self.last_interval == 1: 205 220 if self.last_interval == 1:
self.last_interval = 6 206 221 self.last_interval = 6
else: 207 222 else:
self.last_response_factor = min(1.3, self.last_response_factor+(0.1-(5-q)*(0.08+(5-q)*0.02))) 208 223 self.last_response_factor = min(1.3, self.last_response_factor+(0.1-(5-q)*(0.08+(5-q)*0.02)))
self.last_interval = int(round(self.last_interval*self.last_response_factor)) 209 224 self.last_interval = int(round(self.last_interval*self.last_response_factor))
self.next_review = user_flashcard_quiz.when + timedelta(days=self.last_interval) 210 225 self.next_review = user_flashcard_quiz.when + timedelta(days=self.last_interval)
self.save() 211 226 self.save()
212 227
class Meta: 213 228 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 214 229 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 215 230 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 216 231 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 217 232 # By default, order by most recently pulled
ordering = ['-pulled'] 218 233 ordering = ['-pulled']
219 234
def __unicode__(self): 220 235 def __unicode__(self):
return '%s has %s' % (str(self.user), str(self.flashcard)) 221 236 return '%s has %s' % (str(self.user), str(self.flashcard))
222 237
223 238
class FlashcardHide(Model): 224 239 class FlashcardHide(Model):
""" 225 240 """
Represents the property of a flashcard being hidden by a user. 226 241 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 227 242 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 228 243 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. 229 244 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 230 245 """
user = ForeignKey('User') 231 246 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 232 247 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 233 248 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 234 249 hidden = DateTimeField(auto_now_add=True)
235 250
class Meta: 236 251 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 237 252 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 238 253 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 239 254 index_together = ["user", "flashcard"]
240 255
def __unicode__(self): 241 256 def __unicode__(self):
return '%s hid %s' % (str(self.user), str(self.flashcard)) 242 257 return '%s hid %s' % (str(self.user), str(self.flashcard))
243 258
244 259
class Flashcard(Model): 245 260 class Flashcard(Model):
text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)]) 246 261 text = CharField(max_length=180, help_text='The text on the card', validators=[MinLengthValidator(5)])
section = ForeignKey('Section', help_text='The section with which the card is associated') 247 262 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") 248 263 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") 249 264 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 250 265 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 251 266 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 252 267 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 253 268 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 254 269 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 255 270 help_text="Reason for hiding this card")
mask = MaskField(null=True, blank=True, help_text="The mask on the card") 256 271 mask = MaskField(null=True, blank=True, help_text="The mask on the card")
257 272
class Meta: 258 273 class Meta:
# By default, order by most recently pushed 259 274 # By default, order by most recently pushed
ordering = ['-pushed'] 260 275 ordering = ['-pushed']
261 276
def __unicode__(self): 262 277 def __unicode__(self):
return u'<flashcard: %s>' % self.text 263 278 return u'<flashcard: %s>' % self.text
264 279
@property 265 280 @property
def material_week_num(self): 266 281 def material_week_num(self):
return (self.material_date - QUARTER_START).days / 7 + 1 267 282 return (self.material_date - QUARTER_START).days / 7 + 1
268 283
def is_hidden_from(self, user): 269 284 def is_hidden_from(self, user):
""" 270 285 """
A card can be hidden globally, but if a user has the card in their deck, 271 286 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 272 287 this visibility overrides a global hide.
:param user: 273 288 :param user:
:return: Whether the card is hidden from the user. 274 289 :return: Whether the card is hidden from the user.
""" 275 290 """
return self.is_hidden or self.flashcardhide_set.filter(user=user).exists() 276 291 return self.is_hidden or self.flashcardhide_set.filter(user=user).exists()
277 292
def hide_from(self, user, reason=None): 278 293 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 279 294 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 280 295 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 281 296 if not created:
raise ValidationError("The card has already been hidden.") 282 297 raise ValidationError("The card has already been hidden.")
obj.save() 283 298 obj.save()
284 299
def is_in_deck(self, user): 285 300 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 286 301 return self.userflashcard_set.filter(user=user).exists()
287 302
def add_to_deck(self, user): 288 303 def add_to_deck(self, user):
if not user.is_in_section(self.section): 289 304 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 290 305 raise PermissionDenied("You don't have the permission to add this card")
try: 291 306 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 292 307 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 293 308 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 294 309 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 295 310 user_flashcard.save()
return user_flashcard 296 311 return user_flashcard
297 312
def edit(self, user, new_data): 298 313 def edit(self, user, new_data):
""" 299 314 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 300 315 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. 301 316 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 302 317 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 303 318 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 304 319 """
305 320
# content_changed is True iff either material_date or text were changed 306 321 # content_changed is True iff either material_date or text were changed
content_changed = False 307 322 content_changed = False
# create_new is True iff the user editing this card is the author of this card 308 323 # 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 309 324 # and there are no other users with this card in their decks
create_new = user != self.author or \ 310 325 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 311 326 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 312 327 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 313 328 content_changed = True
self.material_date = new_data['material_date'] 314 329 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 315 330 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 316 331 content_changed = True
self.text = new_data['text'] 317 332 self.text = new_data['text']
if create_new and content_changed: 318 333 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 319 334 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 320 335 self.previous_id = self.pk
self.pk = None 321 336 self.pk = None
self.mask = new_data.get('mask', self.mask) 322 337 self.mask = new_data.get('mask', self.mask)
self.save() 323 338 self.save()
self.add_to_deck(user) 324 339 self.add_to_deck(user)
else: 325 340 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 326 341 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 327 342 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 328 343 user_card.save()
return self 329 344 return self
330 345
def report(self, user, reason=None): 331 346 def report(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 332 347 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 333 348 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 334 349 obj.reason = reason
obj.save() 335 350 obj.save()
336 351
@cached_property 337 352 @cached_property
def score(self): 338 353 def score(self):
def seconds_since_epoch(dt): 339 354 def seconds_since_epoch(dt):
from datetime import datetime 340 355 from datetime import datetime
341 356
epoch = make_aware(datetime.utcfromtimestamp(0)) 342 357 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 343 358 delta = dt - epoch
return delta.total_seconds() 344 359 return delta.total_seconds()
345 360
z = 0 346 361 z = 0
rate = 1.0 / 3600 347 362 rate = 1.0 / 3600
for vote in self.userflashcard_set.iterator(): 348 363 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 349 364 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 350 365 u = max(z, rate * t)
v = min(z, rate * t) 351 366 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 352 367 z = u + log1p(exp(v - u))
return z 353 368 return z
354 369
@classmethod 355 370 @classmethod
def cards_visible_to(cls, user): 356 371 def cards_visible_to(cls, user):
""" 357 372 """
:param user: 358 373 :param user:
:return: A queryset with all cards that should be visible to a user. 359 374 :return: A queryset with all cards that should be visible to a user.
""" 360 375 """
return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user) 361 376 return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)
).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 362 377 ).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
363 378
@classmethod 364 379 @classmethod
def cards_hidden_by(cls, user): 365 380 def cards_hidden_by(cls, user):
return cls.objects.filter(flashcardhide__user=user) 366 381 return cls.objects.filter(flashcardhide__user=user)
367 382
368 383
class UserFlashcardQuiz(Model): 369 384 class UserFlashcardQuiz(Model):
""" 370 385 """
An event of a user being quizzed on a flashcard. 371 386 An event of a user being quizzed on a flashcard.
""" 372 387 """
user_flashcard = ForeignKey(UserFlashcard) 373 388 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 374 389 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=40, blank=True, help_text="The character range which was blanked") 375 390 blanked_word = CharField(max_length=40, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, default=None, help_text="The user's response") 376 391 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") 377 392 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
378 393
def __unicode__(self): 379 394 def __unicode__(self):
return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard)) 380 395 return '%s reviewed %s' % (str(self.user_flashcard.user), str(self.user_flashcard.flashcard))
381 396
def save(self, force_insert=False, force_update=False, using=None, 382 397 def save(self, force_insert=False, force_update=False, using=None,
update_fields=None): 383 398 update_fields=None):
super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update, 384 399 super(UserFlashcardQuiz, self).save(force_insert=force_insert, force_update=force_update,
using=using, update_fields=update_fields) 385 400 using=using, update_fields=update_fields)
self.user_flashcard.review(self) 386 401 self.user_flashcard.review(self)
387 402
def status(self): 388 403 def status(self):
""" 389 404 """
There are three stages of a quiz object: 390 405 There are three stages of a quiz object:
1. the user has been shown the card 391 406 1. the user has been shown the card
2. the user has answered the card 392 407 2. the user has answered the card
3. the user has self-evaluated their response's correctness 393 408 3. the user has self-evaluated their response's correctness
394 409
:return: string (evaluated, answered, viewed) 395 410 :return: string (evaluated, answered, viewed)
""" 396 411 """
if self.correct is not None: return "evaluated" 397 412 if self.correct is not None: return "evaluated"
if self.response: return "answered" 398 413 if self.response: return "answered"
return "viewed" 399 414 return "viewed"
400 415
401 416
class Section(Model): 402 417 class Section(Model):
""" 403 418 """
A UCSD course taught by an instructor during a quarter. 404 419 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 405 420 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 406 421 We index gratuitously to support autofill and because this is primarily read-only
""" 407 422 """
department = CharField(db_index=True, max_length=50) 408 423 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 409 424 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 410 425 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 411 426 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 412 427 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 413 428 quarter = CharField(db_index=True, max_length=4)
PAGE_SIZE = 40 414 429 PAGE_SIZE = 40
415 430
@classmethod 416 431 @classmethod
def search(cls, terms): 417 432 def search(cls, terms):
""" 418 433 """
Search all fields of all sections for a particular set of terms 419 434 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 420 435 A matching section must match at least one field on each term
:param terms:iterable 421 436 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 422 437 :return: Matching QuerySet ordered by department and course number
""" 423 438 """
final_q = Q() 424 439 final_q = Q()
for term in terms: 425 440 for term in terms:
q = Q(department__icontains=term) 426 441 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 427 442 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 428 443 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 429 444 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 430 445 q |= Q(instructor__icontains=term)
final_q &= q 431 446 final_q &= q
qs = cls.objects.filter(final_q) 432 447 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 433 448 # 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 434 449 # 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)"}) 435 450 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 436 451 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 437 452 return qs
438 453
@property 439 454 @property
def is_whitelisted(self): 440 455 def is_whitelisted(self):
""" 441 456 """
:return: whether a whitelist exists for this section 442 457 :return: whether a whitelist exists for this section
""" 443 458 """
return self.whitelist.exists() 444 459 return self.whitelist.exists()
445 460
def is_user_on_whitelist(self, user): 446 461 def is_user_on_whitelist(self, user):
""" 447 462 """
:return: whether the user is on the waitlist for this section 448 463 :return: whether the user is on the waitlist for this section
""" 449 464 """
return self.whitelist.filter(email=user.email).exists() 450 465 return self.whitelist.filter(email=user.email).exists()
451 466
def is_user_enrolled(self, user): 452 467 def is_user_enrolled(self, user):
return self.user_set.filter(pk=user.pk).exists() 453 468 return self.user_set.filter(pk=user.pk).exists()
454 469
def enroll(self, user): 455 470 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 456 471 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 457 472 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 458 473 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 459 474 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 460 475 self.user_set.add(user)
461 476
def drop(self, user): 462 477 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 463 478 if not user.sections.filter(pk=self.pk).exists():
flashcards/serializers.py View file @ f6900af
from json import loads 1 1 from json import loads
from collections import Iterable 2 2 from collections import Iterable
3 3
from django.utils.datetime_safe import datetime 4 4 from django.utils.datetime_safe import datetime
from django.utils.timezone import now 5 5 from django.utils.timezone import now
from flashcards.models import Section, LecturePeriod, User, Flashcard, UserFlashcardQuiz 6 6 from flashcards.models import Section, LecturePeriod, User, Flashcard, 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, empty, \ 9 9 from rest_framework.fields import EmailField, BooleanField, CharField, IntegerField, DateTimeField, empty, \
SerializerMethodField, FloatField 10 10 SerializerMethodField, FloatField
from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField 11 11 from rest_framework.serializers import ModelSerializer, Serializer, PrimaryKeyRelatedField, ListField
from rest_framework.validators import UniqueValidator 12 12 from rest_framework.validators import UniqueValidator
from flashy.settings import QUARTER_END, QUARTER_START 13 13 from flashy.settings import QUARTER_END, QUARTER_START
14 14
15 15
class EmailSerializer(Serializer): 16 16 class EmailSerializer(Serializer):
email = EmailField(required=True) 17 17 email = EmailField(required=True)
18 18
19 19
class EmailPasswordSerializer(EmailSerializer): 20 20 class EmailPasswordSerializer(EmailSerializer):
password = CharField(required=True) 21 21 password = CharField(required=True)
22 22
23 23
class RegistrationSerializer(EmailPasswordSerializer): 24 24 class RegistrationSerializer(EmailPasswordSerializer):
email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())]) 25 25 email = EmailField(required=True, validators=[UniqueValidator(queryset=User.objects.all())])
26 26
27 27
class PasswordResetRequestSerializer(EmailSerializer): 28 28 class PasswordResetRequestSerializer(EmailSerializer):
def validate_email(self, value): 29 29 def validate_email(self, value):
try: 30 30 try:
User.objects.get(email=value) 31 31 User.objects.get(email=value)
return value 32 32 return value
except User.DoesNotExist: 33 33 except User.DoesNotExist:
raise serializers.ValidationError('No user exists with that email') 34 34 raise serializers.ValidationError('No user exists with that email')
35 35
36 36
class PasswordResetSerializer(Serializer): 37 37 class PasswordResetSerializer(Serializer):
new_password = CharField(required=True, allow_blank=False) 38 38 new_password = CharField(required=True, allow_blank=False)
uid = IntegerField(required=True) 39 39 uid = IntegerField(required=True)
token = CharField(required=True) 40 40 token = CharField(required=True)
41 41
def validate_uid(self, value): 42 42 def validate_uid(self, value):
try: 43 43 try:
User.objects.get(id=value) 44 44 User.objects.get(id=value)
return value 45 45 return value
except User.DoesNotExist: 46 46 except User.DoesNotExist:
raise serializers.ValidationError('Could not verify reset token') 47 47 raise serializers.ValidationError('Could not verify reset token')
48 48
49 49
class EmailVerificationSerializer(Serializer): 50 50 class EmailVerificationSerializer(Serializer):
confirmation_key = CharField() 51 51 confirmation_key = CharField()
52 52
53 53
class UserUpdateSerializer(Serializer): 54 54 class UserUpdateSerializer(Serializer):
old_password = CharField(required=False) 55 55 old_password = CharField(required=False)
new_password = CharField(required=False, allow_blank=False) 56 56 new_password = CharField(required=False, allow_blank=False)
57 57
def validate(self, data): 58 58 def validate(self, data):
if 'new_password' in data and 'old_password' not in data: 59 59 if 'new_password' in data and 'old_password' not in data:
raise serializers.ValidationError('old_password is required to set a new_password') 60 60 raise serializers.ValidationError('old_password is required to set a new_password')
return data 61 61 return data
62 62
63 63
class LecturePeriodSerializer(ModelSerializer): 64 64 class LecturePeriodSerializer(ModelSerializer):
class Meta: 65 65 class Meta:
model = LecturePeriod 66 66 model = LecturePeriod
exclude = 'id', 'section' 67 67 exclude = 'id', 'section'
68 68
69 69
class SectionSerializer(ModelSerializer): 70 70 class SectionSerializer(ModelSerializer):
lecture_times = CharField() 71 71 lecture_times = CharField()
short_name = CharField() 72 72 short_name = CharField()
long_name = CharField() 73 73 long_name = CharField()
can_enroll = SerializerMethodField() 74 74 can_enroll = SerializerMethodField()
is_enrolled = SerializerMethodField() 75 75 is_enrolled = SerializerMethodField()
76 76
class Meta: 77 77 class Meta:
model = Section 78 78 model = Section
79 79
def get_can_enroll(self, obj): 80 80 def get_can_enroll(self, obj):
if 'user' not in self.context: return False 81 81 if 'user' not in self.context: return False
if not obj.is_whitelisted: return True 82 82 if not obj.is_whitelisted: return True
return obj.is_user_on_whitelist(self.context['user']) 83 83 return obj.is_user_on_whitelist(self.context['user'])
84 84
def get_is_enrolled(self, obj): 85 85 def get_is_enrolled(self, obj):
if 'user' not in self.context: return False 86 86 if 'user' not in self.context: return False
return obj.is_user_enrolled(self.context['user']) 87 87 return obj.is_user_enrolled(self.context['user'])
88 88
89 89
class DeepSectionSerializer(SectionSerializer): 90 90 class DeepSectionSerializer(SectionSerializer):
lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True) 91 91 lectures = LecturePeriodSerializer(source='lectureperiod_set', many=True, read_only=True)
92 92
93 93
class FeedRequestSerializer(Serializer): 94 94 class FeedRequestSerializer(Serializer):
page = IntegerField(min_value=1, default=1, required=False) 95 95 page = IntegerField(min_value=1, default=1, required=False)
96 96
def validate(self, attrs): 97 97 def validate(self, attrs):
if not isinstance(attrs['page'], int): 98 98 if not isinstance(attrs['page'], int):
raise serializers.ValidationError("Invalid page number") 99 99 raise serializers.ValidationError("Invalid page number")
return attrs 100 100 return attrs
101 101
102 102
class UserSerializer(ModelSerializer): 103 103 class UserSerializer(ModelSerializer):
email = EmailField(required=False) 104 104 email = EmailField(required=False)
sections = SectionSerializer(many=True) 105 105 sections = SectionSerializer(many=True)
106 106
class Meta: 107 107 class Meta:
model = User 108 108 model = User
fields = ("sections", "email", "is_confirmed", "last_login", "date_joined", 'locked') 109 109 fields = ("sections", "email", "is_confirmed", "last_login", "date_joined", 'locked')
110 110
111 111
class MaskFieldSerializer(serializers.Field): 112 112 class MaskFieldSerializer(serializers.Field):
default_error_messages = { 113 113 default_error_messages = {
'max_length': 'Ensure this field has no more than {max_length} characters.', 114 114 'max_length': 'Ensure this field has no more than {max_length} characters.',
'interval': 'Ensure this field has valid intervals.', 115 115 'interval': 'Ensure this field has valid intervals.',
'overlap': 'Ensure this field does not have overlapping intervals.' 116 116 'overlap': 'Ensure this field does not have overlapping intervals.'
} 117 117 }
118 118
def to_representation(self, value): 119 119 def to_representation(self, value):
return map(list, self._make_mask(value)) 120 120 return map(list, self._make_mask(value))
121 121
def to_internal_value(self, value): 122 122 def to_internal_value(self, value):
if not isinstance(value, list): 123 123 if not isinstance(value, list):
value = loads(value) 124 124 value = loads(value)
return self._make_mask(value) 125 125 return self._make_mask(value)
126 126
def _make_mask(self, data): 127 127 def _make_mask(self, data):
try: 128 128 try:
mask = FlashcardMask(data) 129 129 mask = FlashcardMask(data)
except ValueError: 130 130 except ValueError:
raise serializers.ValidationError("Invalid JSON for MaskField") 131 131 raise serializers.ValidationError("Invalid JSON for MaskField")
except TypeError: 132 132 except TypeError:
raise serializers.ValidationError("Invalid data for MaskField.") 133 133 raise serializers.ValidationError("Invalid data for MaskField.")
except OverlapIntervalException: 134 134 except OverlapIntervalException:
raise serializers.ValidationError("Invalid intervals for MaskField data.") 135 135 raise serializers.ValidationError("Invalid intervals for MaskField data.")
if len(mask) > 32: 136 136 if len(mask) > 32:
raise serializers.ValidationError("Too many intervals in the mask.") 137 137 raise serializers.ValidationError("Too many intervals in the mask.")
return mask 138 138 return mask
139 139
140 140
class FlashcardSerializer(ModelSerializer): 141 141 class FlashcardSerializer(ModelSerializer):
is_hidden = SerializerMethodField() 142 142 is_hidden = SerializerMethodField()
is_in_deck = SerializerMethodField() 143 143 is_in_deck = SerializerMethodField()
material_week_num = IntegerField(read_only=True) 144 144 material_week_num = IntegerField(read_only=True)
material_date = DateTimeField(default=now) 145 145 material_date = DateTimeField(default=now)
mask = MaskFieldSerializer(allow_null=True) 146 146 mask = MaskFieldSerializer(allow_null=True)
score = FloatField(read_only=True) 147 147 score = FloatField(read_only=True)
148 148
def validate_material_date(self, value): 149 149 def validate_material_date(self, value):
# TODO: make this dynamic 150 150 # TODO: make this dynamic
if QUARTER_START <= value <= QUARTER_END: 151 151 if QUARTER_START <= value <= QUARTER_END:
return value 152 152 return value
else: 153 153 else:
raise serializers.ValidationError("Material date is outside allowed range for this quarter") 154 154 raise serializers.ValidationError("Material date is outside allowed range for this quarter")
155 155
def validate_pushed(self, value): 156 156 def validate_pushed(self, value):
if value > datetime.now(): 157 157 if value > datetime.now():
raise serializers.ValidationError("Invalid creation date for the Flashcard") 158 158 raise serializers.ValidationError("Invalid creation date for the Flashcard")
return value 159 159 return value
160 160
def validate_mask(self, value): 161 161 def validate_mask(self, value):
if value is None: 162 162 if value is None:
return None 163 163 return None
if len(self.initial_data['text']) < value.max_offset(): 164 164 if len(self.initial_data['text']) < value.max_offset():
raise serializers.ValidationError("Mask out of bounds") 165 165 raise serializers.ValidationError("Mask out of bounds")
return value 166 166 return value
167 167
def get_is_hidden(self, obj): 168 168 def get_is_hidden(self, obj):
if 'user' not in self.context: return False 169 169 if 'user' not in self.context: return False
return obj.is_hidden_from(self.context['user']) 170 170 return obj.is_hidden_from(self.context['user'])
171 171
def get_is_in_deck(self, obj): 172 172 def get_is_in_deck(self, obj):
if 'user' not in self.context: return False 173 173 if 'user' not in self.context: return False
return obj.is_in_deck(self.context['user']) 174 174 return obj.is_in_deck(self.context['user'])
175 175
class Meta: 176 176 class Meta:
model = Flashcard 177 177 model = Flashcard
exclude = 'author', 'previous', 'hide_reason' 178 178 exclude = 'author', 'previous', 'hide_reason'
179 179
180 180
class FlashcardUpdateSerializer(serializers.Serializer): 181 181 class FlashcardUpdateSerializer(serializers.Serializer):
text = CharField(max_length=255, required=False) 182 182 text = CharField(max_length=255, required=False)
material_date = DateTimeField(required=False) 183 183 material_date = DateTimeField(required=False)
mask = MaskFieldSerializer(required=False) 184 184 mask = MaskFieldSerializer(required=False)
185 185
def validate_material_date(self, date): 186 186 def validate_material_date(self, date):
if date > QUARTER_END: 187 187 if date > QUARTER_END:
raise serializers.ValidationError("Invalid material_date for the flashcard") 188 188 raise serializers.ValidationError("Invalid material_date for the flashcard")
return date 189 189 return date
190 190
def validate(self, attrs): 191 191 def validate(self, attrs):
# Make sure that at least one of the attributes was passed in 192 192 # 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']): 193 193 if not any(i in attrs for i in ['material_date', 'text', 'mask']):
raise serializers.ValidationError("No new value passed in") 194 194 raise serializers.ValidationError("No new value passed in")
return attrs 195 195 return attrs
196 196
197 197
class QuizRequestSerializer(serializers.Serializer): 198 198 class QuizRequestSerializer(serializers.Serializer):
sections = ListField(child=IntegerField(min_value=1), required=False) 199 199 sections = ListField(child=IntegerField(min_value=1), required=False)
material_date_begin = DateTimeField(default=QUARTER_START) 200 200 material_date_begin = DateTimeField(default=QUARTER_START)
material_date_end = DateTimeField(default=QUARTER_END) 201 201 material_date_end = DateTimeField(default=QUARTER_END)
202 202
def update(self, instance, validated_data): 203 203 def update(self, instance, validated_data):
pass 204 204 pass
205 205
def create(self, validated_data): 206 206 def create(self, validated_data):
return validated_data 207 207 return validated_data
208 208
def validate_material_date_begin(self, value): 209 209 def validate_material_date_begin(self, value):
if QUARTER_START <= value <= QUARTER_END: 210 210 if QUARTER_START <= value <= QUARTER_END:
return value 211 211 return value
raise serializers.ValidationError("Invalid begin date for the flashcard range") 212 212 raise serializers.ValidationError("Invalid begin date for the flashcard range")
213 213
def validate_material_date_end(self, value): 214 214 def validate_material_date_end(self, value):
if QUARTER_START <= value <= QUARTER_END: 215 215 if QUARTER_START <= value <= QUARTER_END:
return value 216 216 return value
raise serializers.ValidationError("Invalid end date for the flashcard range") 217 217 raise serializers.ValidationError("Invalid end date for the flashcard range")
218 218
def validate_sections(self, value): 219 219 def validate_sections(self, value):
if value is not None and not isinstance(value, Iterable): 220 220 if value is not None and not isinstance(value, Iterable):
raise serializers.ValidationError("Invalid section format. Expecting a list or no value.") 221 221 raise serializers.ValidationError("Invalid section format. Expecting a list or no value.")
if value is None or len(value) == 0: 222 222 if value is None or len(value) == 0:
return Section.objects.all() 223 223 return Section.objects.all()
section_filter = Section.objects.filter(pk__in=value) 224 224 section_filter = Section.objects.filter(pk__in=value)
if not section_filter.exists(): 225 225 if not section_filter.exists():
raise serializers.ValidationError("Those aren't valid sections") 226 226 raise serializers.ValidationError("Those aren't valid sections")
return value 227 227 return value
228 228
def validate(self, attrs): 229 229 def validate(self, attrs):
if attrs['material_date_begin'] > attrs['material_date_end']: 230 230 if attrs['material_date_begin'] > attrs['material_date_end']:
flashcards/views.py View file @ f6900af
import django 1 1 import django
from django.contrib import auth 2 2 from django.contrib import auth
from django.shortcuts import get_object_or_404 3 3 from django.shortcuts import get_object_or_404
from django.utils.log import getLogger 4 4 from django.utils.log import getLogger
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \ 5 5 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer, \
IsAuthenticatedAndConfirmed 6 6 IsAuthenticatedAndConfirmed
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \ 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz, \
FlashcardAlreadyPulledException, FlashcardNotInDeckException 8 8 FlashcardAlreadyPulledException, FlashcardNotInDeckException
from flashcards.notifications import notify_new_card, notify_pull 9 9 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 10 10 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 11 11 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 12 12 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, SubscribeViewSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer 13 13 QuizAnswerRequestSerializer, DeepSectionSerializer, EmailVerificationSerializer, FeedRequestSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 14 14 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 15 15 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 16 16 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 17 17 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 18 18 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 19 19 from django.core.mail import send_mail
from django.contrib.auth import authenticate 20 20 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 21 21 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 22 22 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 23 23 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 24 24 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 25 25 from simple_email_confirmation import EmailAddress
26 26
27 27
def log_event(request, event=''): 28 28 def log_event(request, event=''):
logstr = u'%s %s %s %s' % (request.META['REMOTE_ADDR'], request.user, request.path, event) 29 29 logstr = u'%s %s %s %s' % (request.META['REMOTE_ADDR'], request.user, request.path, event)
getLogger('flashy.events').info(logstr) 30 30 getLogger('flashy.events').info(logstr)
31 31
32 32
class SectionViewSet(ReadOnlyModelViewSet): 33 33 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 34 34 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 35 35 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 36 36 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticatedAndConfirmed] 37 37 permission_classes = [IsAuthenticatedAndConfirmed]
38 38
@detail_route(methods=['GET']) 39 39 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 40 40 def flashcards(self, request, pk):
""" 41 41 """
Gets flashcards for a section, excluding hidden cards. 42 42 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 43 43 Returned in strictly chronological order (material date).
""" 44 44 """
flashcards = Flashcard.cards_visible_to(request.user) 45 45 flashcards = Flashcard.cards_visible_to(request.user)
if 'hidden' in request.GET: 46 46 if 'hidden' in request.GET:
if request.GET['hidden'] == 'only': 47 47 if request.GET['hidden'] == 'only':
flashcards = Flashcard.cards_hidden_by(request.user) 48 48 flashcards = Flashcard.cards_hidden_by(request.user)
else: 49 49 else:
flashcards |= Flashcard.cards_hidden_by(request.user) 50 50 flashcards |= Flashcard.cards_hidden_by(request.user)
flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all() 51 51 flashcards = flashcards.filter(section=self.get_object()).order_by('material_date').all()
log_event(request, str(self.get_object())) 52 52 log_event(request, str(self.get_object()))
return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data) 53 53 return Response(FlashcardSerializer(flashcards, context={"user": request.user}, many=True).data)
54 54
@detail_route(methods=['POST']) 55 55 @detail_route(methods=['POST'])
def enroll(self, request, pk): 56 56 def enroll(self, request, pk):
57 57
""" 58 58 """
Add the current user to a specified section 59 59 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. 60 60 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 61 61 ---
view_mocker: flashcards.api.mock_no_params 62 62 view_mocker: flashcards.api.mock_no_params
""" 63 63 """
try: 64 64 try:
self.get_object().enroll(request.user) 65 65 self.get_object().enroll(request.user)
log_event(request, str(self.get_object())) 66 66 log_event(request, str(self.get_object()))
except django.core.exceptions.PermissionDenied as e: 67 67 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 68 68 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 69 69 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 70 70 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 71 71 return Response(status=HTTP_204_NO_CONTENT)
72 72
@detail_route(methods=['POST']) 73 73 @detail_route(methods=['POST'])
def drop(self, request, pk): 74 74 def drop(self, request, pk):
""" 75 75 """
Remove the current user from a specified section 76 76 Remove the current user from a specified section
If the user is not in the class, the request will fail. 77 77 If the user is not in the class, the request will fail.
--- 78 78 ---
view_mocker: flashcards.api.mock_no_params 79 79 view_mocker: flashcards.api.mock_no_params
""" 80 80 """
try: 81 81 try:
self.get_object().drop(request.user) 82 82 self.get_object().drop(request.user)
log_event(request, str(self.get_object())) 83 83 log_event(request, str(self.get_object()))
except django.core.exceptions.PermissionDenied as e: 84 84 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 85 85 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 86 86 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 87 87 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 88 88 return Response(status=HTTP_204_NO_CONTENT)
89 89
@list_route(methods=['GET']) 90 90 @list_route(methods=['GET'])
def search(self, request): 91 91 def search(self, request):
""" 92 92 """
Returns a list of sections which match a user's query 93 93 Returns a list of sections which match a user's query
--- 94 94 ---
parameters: 95 95 parameters:
- name: q 96 96 - name: q
description: space-separated list of terms 97 97 description: space-separated list of terms
required: true 98 98 required: true
type: form 99 99 type: form
response_serializer: SectionSerializer 100 100 response_serializer: SectionSerializer
""" 101 101 """
query = request.GET.get('q', None) 102 102 query = request.GET.get('q', None)
if not query: return Response('[]') 103 103 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 104 104 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True, context={'user': request.user}).data 105 105 data = SectionSerializer(qs, many=True, context={'user': request.user}).data
log_event(request, query) 106 106 log_event(request, query)
return Response(data) 107 107 return Response(data)
108 108
@detail_route(methods=['GET']) 109 109 @detail_route(methods=['GET'])
def deck(self, request, pk): 110 110 def deck(self, request, pk):
""" 111 111 """
Gets the contents of a user's deck for a given section. 112 112 Gets the contents of a user's deck for a given section.
""" 113 113 """
qs = request.user.get_deck(self.get_object()) 114 114 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 115 115 serializer = FlashcardSerializer(qs, many=True)
log_event(request, str(self.get_object())) 116 116 log_event(request, str(self.get_object()))
return Response(serializer.data) 117 117 return Response(serializer.data)
118 118
@detail_route(methods=['GET']) 119 119 @detail_route(methods=['GET'])
def feed(self, request, pk): 120 120 def feed(self, request, pk):
""" 121 121 """
Gets the contents of a user's feed for a section. 122 122 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 123 123 Exclude cards that are already in the user's deck
request_serializer: FeedRequestSerializer 124 124 request_serializer: FeedRequestSerializer
response_serializer: FlashcardSerializer 125 125 response_serializer: FlashcardSerializer
""" 126 126 """
feed_serializer = FeedRequestSerializer(data=request.data) 127 127 feed_serializer = FeedRequestSerializer(data=request.data)
feed_serializer.is_valid(raise_exception=True) 128 128 feed_serializer.is_valid(raise_exception=True)
page = feed_serializer.validated_data['page'] 129 129 page = feed_serializer.validated_data['page']
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user, page=page), 130 130 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user, page=page),
many=True, context={'user': request.user}) 131 131 many=True, context={'user': request.user})
log_event(request, str(self.get_object())) 132 132 log_event(request, str(self.get_object()))
return Response(serializer.data) 133 133 return Response(serializer.data)
134 134
135 135
class UserSectionListView(ListAPIView): 136 136 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 137 137 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticatedAndConfirmed] 138 138 permission_classes = [IsAuthenticatedAndConfirmed]
139 139
def get_queryset(self): 140 140 def get_queryset(self):
return self.request.user.sections.all() 141 141 return self.request.user.sections.all()
142 142
def paginate_queryset(self, queryset): return None 143 143 def paginate_queryset(self, queryset): return None
144 144
145 145
class UserDetail(GenericAPIView): 146 146 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 147 147 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 148 148 permission_classes = [IsAuthenticated]
149 149
def patch(self, request, format=None): 150 150 def patch(self, request, format=None):
""" 151 151 """
Updates the user's password 152 152 Updates the user's password
--- 153 153 ---
request_serializer: UserUpdateSerializer 154 154 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 155 155 response_serializer: UserSerializer
""" 156 156 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 157 157 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 158 158 data.is_valid(raise_exception=True)
data = data.validated_data 159 159 data = data.validated_data
160 160
if 'new_password' in data: 161 161 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 162 162 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 163 163 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 164 164 request.user.set_password(data['new_password'])
request.user.save() 165 165 request.user.save()
log_event(request, 'change password') 166 166 log_event(request, 'change password')
167 167
return Response(UserSerializer(request.user).data) 168 168 return Response(UserSerializer(request.user).data)
169 169
def get(self, request, format=None): 170 170 def get(self, request, format=None):
""" 171 171 """
Return data about the user 172 172 Return data about the user
--- 173 173 ---
response_serializer: UserSerializer 174 174 response_serializer: UserSerializer
""" 175 175 """
serializer = UserSerializer(request.user, context={'request': request}) 176 176 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 177 177 return Response(serializer.data)
178 178
def delete(self, request): 179 179 def delete(self, request):
""" 180 180 """
Irrevocably delete the user and their data 181 181 Irrevocably delete the user and their data
182 182
Yes, really 183 183 Yes, really
""" 184 184 """
request.user.delete() 185 185 request.user.delete()
log_event(request) 186 186 log_event(request)
return Response(status=HTTP_204_NO_CONTENT) 187 187 return Response(status=HTTP_204_NO_CONTENT)
188 188
189 189
@api_view(['POST']) 190 190 @api_view(['POST'])
@permission_classes([IsAuthenticated]) 191 191 @permission_classes([IsAuthenticated])
def resend_confirmation_email(request): 192 192 def resend_confirmation_email(request):
"Resends a confirmation email to a user" 193 193 "Resends a confirmation email to a user"
request.user.send_confirmation_email() 194 194 request.user.send_confirmation_email()
return Response(status=HTTP_204_NO_CONTENT) 195 195 return Response(status=HTTP_204_NO_CONTENT)
196 196
197 197
@api_view(['POST']) 198 198 @api_view(['POST'])
@permission_classes([IsAuthenticated]) 199 199 @permission_classes([IsAuthenticated])
def verify_email(request): 200 200 def verify_email(request):
""" 201 201 """
Accepts a user's email confirmation_key to verify their email address 202 202 Accepts a user's email confirmation_key to verify their email address
--- 203 203 ---
request_serializer: EmailVerificationSerializer 204 204 request_serializer: EmailVerificationSerializer
""" 205 205 """
206 206
data = EmailVerificationSerializer(data=request.data) 207 207 data = EmailVerificationSerializer(data=request.data)
data.is_valid(raise_exception=True) 208 208 data.is_valid(raise_exception=True)
try: 209 209 try:
email = User.confirm_email(data.validated_data['confirmation_key']) 210 210 email = User.confirm_email(data.validated_data['confirmation_key'])
except EmailAddress.DoesNotExist: 211 211 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 212 212 raise ValidationError('confirmation_key is invalid')
log_event(request, 'confirm email' + str(email)) 213 213 log_event(request, 'confirm email' + str(email))
return Response(status=HTTP_204_NO_CONTENT) 214 214 return Response(status=HTTP_204_NO_CONTENT)
215 215
216 216
@api_view(['POST']) 217 217 @api_view(['POST'])
def register(request, format=None): 218 218 def register(request, format=None):
""" 219 219 """
Register a new user 220 220 Register a new user
--- 221 221 ---
request_serializer: EmailPasswordSerializer 222 222 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 223 223 response_serializer: UserSerializer
""" 224 224 """
data = RegistrationSerializer(data=request.data) 225 225 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 226 226 data.is_valid(raise_exception=True)
227 227
User.objects.create_user(**data.validated_data) 228 228 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 229 229 user = authenticate(**data.validated_data)
auth.login(request, user) 230 230 auth.login(request, user)
log_event(request) 231 231 log_event(request)
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 232 232 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
233
234
235 @api_view(['POST'])
236 def subscribe(request, format=None):
237 """
238 Associate the user with the passed in registration token
239 ---
240 request_serializer: SubscribeViewSerializer
241 """
242 serializer = SubscribeViewSerializer(data=request.data)
243 serializer.is_valid(raise_exception=True)
244 request.user.set_registration_token(serializer.validated_data['registration_token'])
245 return Response(status=HTTP_204_NO_CONTENT)
233 246
234 247
@api_view(['POST']) 235 248 @api_view(['POST'])
def login(request): 236 249 def login(request):
""" 237 250 """
Authenticates user and returns user data if valid. 238 251 Authenticates user and returns user data if valid.
--- 239 252 ---
request_serializer: EmailPasswordSerializer 240 253 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 241 254 response_serializer: UserSerializer
""" 242 255 """
243 256
data = EmailPasswordSerializer(data=request.data) 244 257 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 258 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 246 259 user = authenticate(**data.validated_data)
247 260
if user is None: 248 261 if user is None:
raise AuthenticationFailed('Invalid email or password') 249 262 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 250 263 if not user.is_active:
raise NotAuthenticated('Account is disabled') 251 264 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 252 265 auth.login(request, user)
log_event(request) 253 266 log_event(request)
return Response(UserSerializer(request.user).data) 254 267 return Response(UserSerializer(request.user).data)
255 268
256 269
@api_view(['POST']) 257 270 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 258 271 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 259 272 def logout(request, format=None):
""" 260 273 """
Logs the authenticated user out. 261 274 Logs the authenticated user out.
""" 262 275 """
auth.logout(request) 263 276 auth.logout(request)
log_event(request) 264 277 log_event(request)
return Response(status=HTTP_204_NO_CONTENT) 265 278 return Response(status=HTTP_204_NO_CONTENT)
266 279
267 280
@api_view(['POST']) 268 281 @api_view(['POST'])
def request_password_reset(request, format=None): 269 282 def request_password_reset(request, format=None):
""" 270 283 """
Send a password reset token/link to the provided email. 271 284 Send a password reset token/link to the provided email.
--- 272 285 ---
request_serializer: PasswordResetRequestSerializer 273 286 request_serializer: PasswordResetRequestSerializer
""" 274 287 """
data = PasswordResetRequestSerializer(data=request.data) 275 288 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 276 289 data.is_valid(raise_exception=True)
log_event(request, 'email: ' + str(data['email'])) 277 290 log_event(request, 'email: ' + str(data['email']))
get_object_or_404(User, email=data['email'].value).request_password_reset() 278 291 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 279 292 return Response(status=HTTP_204_NO_CONTENT)
280 293
281 294
@api_view(['POST']) 282 295 @api_view(['POST'])
def reset_password(request, format=None): 283 296 def reset_password(request, format=None):
""" 284 297 """
Updates user's password to new password if token is valid. 285 298 Updates user's password to new password if token is valid.
--- 286 299 ---
request_serializer: PasswordResetSerializer 287 300 request_serializer: PasswordResetSerializer
""" 288 301 """
data = PasswordResetSerializer(data=request.data) 289 302 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 290 303 data.is_valid(raise_exception=True)
291 304
user = User.objects.get(id=data['uid'].value) 292 305 user = User.objects.get(id=data['uid'].value)
# Check token validity. 293 306 # Check token validity.
294 307
if default_token_generator.check_token(user, data['token'].value): 295 308 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 296 309 user.set_password(data['new_password'].value)
user.save() 297 310 user.save()
log_event(request) 298 311 log_event(request)
else: 299 312 else:
raise ValidationError('Could not verify reset token') 300 313 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 301 314 return Response(status=HTTP_204_NO_CONTENT)
302 315
303 316
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 304 317 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 305 318 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 306 319 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection] 307 320 permission_classes = [IsAuthenticatedAndConfirmed, IsEnrolledInAssociatedSection]
308 321
# Override create in CreateModelMixin 309 322 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 310 323 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 311 324 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 312 325 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 313 326 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 314 327 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 315 328 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 316 329 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 317 330 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 318 331 self.perform_create(flashcard)
notify_new_card(flashcard) 319 332 notify_new_card(flashcard)
headers = self.get_success_headers(data) 320 333 headers = self.get_success_headers(data)
request.user.pull(flashcard) 321 334 request.user.pull(flashcard)
response_data = FlashcardSerializer(flashcard).data 322 335 response_data = FlashcardSerializer(flashcard).data
log_event(request, response_data) 323 336 log_event(request, response_data)
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 324 337 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
325 338
@detail_route(methods=['POST']) 326 339 @detail_route(methods=['POST'])
def unhide(self, request, pk): 327 340 def unhide(self, request, pk):
""" 328 341 """
Unhide the given card 329 342 Unhide the given card
--- 330 343 ---
view_mocker: flashcards.api.mock_no_params 331 344 view_mocker: flashcards.api.mock_no_params
""" 332 345 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 333 346 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 334 347 hide.delete()
log_event(request, str(self.get_object())) 335 348 log_event(request, str(self.get_object()))
return Response(status=HTTP_204_NO_CONTENT) 336 349 return Response(status=HTTP_204_NO_CONTENT)
337 350
@detail_route(methods=['POST']) 338 351 @detail_route(methods=['POST'])
def report(self, request, pk): 339 352 def report(self, request, pk):
""" 340 353 """
Hide the given card 341 354 Hide the given card
--- 342 355 ---
view_mocker: flashcards.api.mock_no_params 343 356 view_mocker: flashcards.api.mock_no_params
""" 344 357 """
self.get_object().report(request.user) 345 358 self.get_object().report(request.user)
log_event(request, str(self.get_object())) 346 359 log_event(request, str(self.get_object()))
return Response(status=HTTP_204_NO_CONTENT) 347 360 return Response(status=HTTP_204_NO_CONTENT)
348 361
hide = report 349 362 hide = report
350 363
@detail_route(methods=['POST']) 351 364 @detail_route(methods=['POST'])
def pull(self, request, pk): 352 365 def pull(self, request, pk):
""" 353 366 """
Pull a card from the live feed into the user's deck. 354 367 Pull a card from the live feed into the user's deck.
--- 355 368 ---
view_mocker: flashcards.api.mock_no_params 356 369 view_mocker: flashcards.api.mock_no_params
""" 357 370 """
try: 358 371 try:
request.user.pull(self.get_object()) 359 372 request.user.pull(self.get_object())
log_event(request, self.get_object()) 360 373 log_event(request, self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 361 374 return Response(status=HTTP_204_NO_CONTENT)
except FlashcardAlreadyPulledException: 362 375 except FlashcardAlreadyPulledException:
raise ValidationError('Cannot pull a card already in deck') 363 376 raise ValidationError('Cannot pull a card already in deck')
364 377
@detail_route(methods=['POST']) 365 378 @detail_route(methods=['POST'])
def unpull(self, request, pk): 366 379 def unpull(self, request, pk):
""" 367 380 """
Unpull a card from the user's deck 368 381 Unpull a card from the user's deck
--- 369 382 ---
view_mocker: flashcards.api.mock_no_params 370 383 view_mocker: flashcards.api.mock_no_params
""" 371 384 """
user = request.user 372 385 user = request.user
flashcard = self.get_object() 373 386 flashcard = self.get_object()
try: 374 387 try:
user.unpull(flashcard) 375 388 user.unpull(flashcard)
log_event(request, self.get_object()) 376 389 log_event(request, self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 377 390 return Response(status=HTTP_204_NO_CONTENT)
except FlashcardNotInDeckException: 378 391 except FlashcardNotInDeckException:
raise ValidationError('Cannot unpull a card not in deck') 379 392 raise ValidationError('Cannot unpull a card not in deck')
380 393
def partial_update(self, request, *args, **kwargs): 381 394 def partial_update(self, request, *args, **kwargs):
""" 382 395 """
Edit settings related to a card for the user. 383 396 Edit settings related to a card for the user.
--- 384 397 ---
request_serializer: FlashcardUpdateSerializer 385 398 request_serializer: FlashcardUpdateSerializer
""" 386 399 """
user = request.user 387 400 user = request.user
flashcard = self.get_object() 388 401 flashcard = self.get_object()
flashy/settings.py View file @ f6900af
from datetime import datetime 1 1 from datetime import datetime
2 2
import os 3 3 import os
from pytz import UTC 4 4 from pytz import UTC
5 5
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 6 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
7 7
IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ 8 8 IN_PRODUCTION = 'FLASHY_PRODUCTION' in os.environ
9 9
DEBUG = not IN_PRODUCTION 10 10 DEBUG = not IN_PRODUCTION
11 11
ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards'] 12 12 ALLOWED_HOSTS = ['127.0.0.1', 'flashy.cards']
13 13
if DEBUG: 14 14 if DEBUG:
ABSOLUTE_URL_ROOT = 'http://127.0.0.1:8080/' 15 15 ABSOLUTE_URL_ROOT = 'http://127.0.0.1:8080/'
else: 16 16 else:
ABSOLUTE_URL_ROOT = 'https://flashy.cards/' 17 17 ABSOLUTE_URL_ROOT = 'https://flashy.cards/'
18 18
AUTH_USER_MODEL = 'flashcards.User' 19 19 AUTH_USER_MODEL = 'flashcards.User'
REST_FRAMEWORK = { 20 20 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 21 21 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20 22 22 'PAGE_SIZE': 20
} 23 23 }
24 24
INSTALLED_APPS = [ 25 25 INSTALLED_APPS = [
'simple_email_confirmation', 26 26 'simple_email_confirmation',
'flashcards', 27 27 'flashcards',
'django.contrib.admin', 28 28 'django.contrib.admin',
'django.contrib.admindocs', 29 29 'django.contrib.admindocs',
'django.contrib.auth', 30 30 'django.contrib.auth',
'django.contrib.contenttypes', 31 31 'django.contrib.contenttypes',
'django.contrib.sessions', 32 32 'django.contrib.sessions',
'django.contrib.messages', 33 33 'django.contrib.messages',
'django.contrib.staticfiles', 34 34 'django.contrib.staticfiles',
'ws4redis', 35 35 'ws4redis',
36 36
'rest_framework_swagger', 37 37 'rest_framework_swagger',
'rest_framework', 38 38 'rest_framework',
'django_extensions', 39 39 'django_extensions',
] 40 40 ]
41 41
WEBSOCKET_URL = '/ws/' 42 42 WEBSOCKET_URL = '/ws/'
43 43
MIDDLEWARE_CLASSES = ( 44 44 MIDDLEWARE_CLASSES = (
'flashcards.middleware.SetRemoteAddrFromForwardedFor', 45 45 'flashcards.middleware.SetRemoteAddrFromForwardedFor',
'django.contrib.sessions.middleware.SessionMiddleware', 46 46 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 47 47 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 48 48 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 49 49 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 50 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 51 51 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 52 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 53 53 'django.middleware.security.SecurityMiddleware',
54 54
) 55 55 )
56 56
ROOT_URLCONF = 'flashy.urls' 57 57 ROOT_URLCONF = 'flashy.urls'
58 58
AUTHENTICATION_BACKENDS = ( 59 59 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 60 60 'django.contrib.auth.backends.ModelBackend',
) 61 61 )
62 62
TEMPLATES = [ 63 63 TEMPLATES = [
{ 64 64 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 65 65 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 66 66 'DIRS': ['templates/'],
'APP_DIRS': True, 67 67 'APP_DIRS': True,
'OPTIONS': { 68 68 'OPTIONS': {
'context_processors': [ 69 69 'context_processors': [
'django.template.context_processors.debug', 70 70 'django.template.context_processors.debug',
'django.template.context_processors.request', 71 71 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 72 72 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 73 73 'django.contrib.messages.context_processors.messages',
'django.core.context_processors.static', 74 74 'django.core.context_processors.static',
'ws4redis.context_processors.default', 75 75 'ws4redis.context_processors.default',
], 76 76 ],
}, 77 77 },
}, 78 78 },
] 79 79 ]
80 80
WSGI_APPLICATION = 'ws4redis.django_runserver.application' 81 81 WSGI_APPLICATION = 'ws4redis.django_runserver.application'
82 82
DATABASES = { 83 83 DATABASES = {
'default': { 84 84 'default': {
'ENGINE': 'django.db.backends.sqlite3', 85 85 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 86 86 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 87 87 }
} 88 88 }
89 89
if IN_PRODUCTION: 90 90 if IN_PRODUCTION:
DATABASES['default'] = { 91 91 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 92 92 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 93 93 'NAME': 'flashy',
'USER': 'flashy', 94 94 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 95 95 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 96 96 'HOST': 'localhost',
'PORT': '', 97 97 'PORT': '',
} 98 98 }
99 99
LANGUAGE_CODE = 'en-us' 100 100 LANGUAGE_CODE = 'en-us'
USE_I18N = True 101 101 USE_I18N = True
USE_L10N = True 102 102 USE_L10N = True
USE_TZ = True 103 103 USE_TZ = True
TIME_ZONE = 'America/Los_Angeles' 104 104 TIME_ZONE = 'America/Los_Angeles'
105 105
QUARTER_START = UTC.localize(datetime(2015, 3, 30)) 106 106 QUARTER_START = UTC.localize(datetime(2015, 3, 30))
QUARTER_END = UTC.localize(datetime(2015, 6, 12)) 107 107 QUARTER_END = UTC.localize(datetime(2015, 6, 12))
108 108
STATIC_URL = '/static/' 109 109 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 110 110 STATIC_ROOT = 'static'
111 111
# Four settings just to be sure 112 112 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 113 113 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 114 114 EMAIL_HOST_USER = 'noreply@flashy.cards'
DEFAULT_FROM_EMAIL = 'noreply@flashy.cards' 115 115 DEFAULT_FROM_EMAIL = 'noreply@flashy.cards'
SERVER_EMAIL = 'noreply@flashy.cards' 116 116 SERVER_EMAIL = 'noreply@flashy.cards'
117 117
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 118 118 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
119 119
if IN_PRODUCTION: 120 120 if IN_PRODUCTION:
INSTALLED_APPS.append('django_ses') 121 121 INSTALLED_APPS.append('django_ses')
AWS_SES_REGION_NAME = 'us-west-2' 122 122 AWS_SES_REGION_NAME = 'us-west-2'
AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com' 123 123 AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com'
EMAIL_BACKEND = 'django_ses.SESBackend' 124 124 EMAIL_BACKEND = 'django_ses.SESBackend'
125 125
if IN_PRODUCTION: 126 126 if IN_PRODUCTION:
SESSION_COOKIE_SECURE = True 127 127 SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True 128 128 CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 129 129 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# are we secure yet? 130 130 # are we secure yet?
131 131
if IN_PRODUCTION: 132 132 if IN_PRODUCTION:
LOGGING = { 133 133 LOGGING = {
'version': 1, 134 134 'version': 1,
'disable_existing_loggers': False, 135 135 'disable_existing_loggers': False,
'handlers': { 136 136 'handlers': {
'file': { 137 137 'file': {
'level': 'DEBUG', 138 138 'level': 'DEBUG',
'class': 'logging.FileHandler', 139 139 'class': 'logging.FileHandler',
'filename': 'debug.log', 140 140 'filename': 'debug.log',
}, 141 141 },
'eventslog': { 142 142 'eventslog': {
'level': 'INFO', 143 143 'level': 'INFO',
'class': 'logging.FileHandler', 144 144 'class': 'logging.FileHandler',
'filename': 'events.log', 145 145 'filename': 'events.log',
'formatter': 'verbose' 146 146 'formatter': 'verbose'
}, 147 147 },
}, 148 148 },
'formatters': { 149 149 'formatters': {
'verbose': { 150 150 'verbose': {
'format': '%(asctime)s %(module)s %(message)s' 151 151 'format': '%(asctime)s %(module)s %(message)s'
}, 152 152 },
}, 153 153 },
'loggers': { 154 154 'loggers': {
flashy/urls.py View file @ f6900af
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, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email 4 4 reset_password, logout, login, register, UserFlashcardQuizViewSet, resend_confirmation_email, verify_email, subscribe
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)
router.register(r'study', UserFlashcardQuizViewSet) 13 13 router.register(r'study', UserFlashcardQuizViewSet)
14 14
urlpatterns = [ 15 15 urlpatterns = [
url(r'^api/docs/', include('rest_framework_swagger.urls')), 16 16 url(r'^api/docs/', include('rest_framework_swagger.urls')),
url(r'^api/me/$', UserDetail.as_view()), 17 17 url(r'^api/me/$', UserDetail.as_view()),
url(r'^api/register/', register), 18 18 url(r'^api/register/', register),
url(r'^api/login/$', login), 19 19 url(r'^api/login/$', login),
url(r'^api/logout/$', logout), 20 20 url(r'^api/logout/$', logout),
21 url(r'^api/subscribe/$', subscribe),
url(r'^api/me/sections/', UserSectionListView.as_view()), 21 22 url(r'^api/me/sections/', UserSectionListView.as_view()),
url(r'^api/resend_confirmation_email/', resend_confirmation_email), 22 23 url(r'^api/resend_confirmation_email/', resend_confirmation_email),
url(r'^api/verify_email/', verify_email), 23 24 url(r'^api/verify_email/', verify_email),
url(r'^api/request_password_reset/', request_password_reset), 24 25 url(r'^api/request_password_reset/', request_password_reset),
url(r'^api/reset_password/', reset_password), 25 26 url(r'^api/reset_password/', reset_password),
url(r'^api/', include(router.urls)), 26 27 url(r'^api/', include(router.urls)),
url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 27 28 url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)), 28 29 url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), 29 30 url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
] 30 31 ]
31 32
if IN_PRODUCTION: 32 33 if IN_PRODUCTION:
urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),) 33 34 urlpatterns += (url(r'^admin/django-ses/', include('django_ses.urls')),)
34 35
requirements.txt View file @ f6900af
#beautifulsoup4 1 1 #beautifulsoup4
Django>=1.8 2 2 Django>=1.8
django-websocket-redis 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
cached-property 8 8 cached-property
djangorestframework 9 9 djangorestframework
docutils 10 10 docutils
django-simple-email-confirmation 11 11 django-simple-email-confirmation
coverage 12 12 coverage
django-rest-swagger 13 13 django-rest-swagger
pytz 14 14 pytz
django-extensions 15 15 django-extensions
django-redis-sessions 16 16 django-redis-sessions
17 python-gcm