Commit f53a10aaffbb577352a97b225adbf195fb277e72

Authored by Andrew Buss
1 parent 057d2cc3f7
Exists in master

add django-extensions

Showing 4 changed files with 7 additions and 10 deletions Inline Diff

flashcards/models.py View file @ f53a10a
from math import log1p 1 1 from math import log1p
from math import exp 2 2 from math import exp
3 3
from django.contrib.auth.models import AbstractUser, UserManager 4 4 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 5 5 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 6 6 from django.core.cache import cache
from django.core.exceptions import ValidationError 7 7 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 8 8 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 9 9 from django.core.mail import send_mail
from django.db import IntegrityError 10 10 from django.db import IntegrityError
from django.db.models import * 11 11 from django.db.models import *
from django.utils.timezone import now, make_aware 12 12 from django.utils.timezone import now, make_aware
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 13 13 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 14 14 from fields import MaskField
from cached_property import cached_property 15 15 from cached_property import cached_property
16 16
17 17
18 18
19 19
20 20
21 21
22 22
23 23
# Hack to fix AbstractUser before subclassing it 24 24 # Hack to fix AbstractUser before subclassing it
25 25
AbstractUser._meta.get_field('email')._unique = True 26 26 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 27 27 AbstractUser._meta.get_field('username')._unique = False
28 28
29 29
class EmailOnlyUserManager(UserManager): 30 30 class EmailOnlyUserManager(UserManager):
""" 31 31 """
A tiny extension of Django's UserManager which correctly creates users 32 32 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 33 33 without usernames (using emails instead).
""" 34 34 """
35 35
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 36 36 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 37 37 """
Creates and saves a User with the given email and password. 38 38 Creates and saves a User with the given email and password.
""" 39 39 """
email = self.normalize_email(email) 40 40 email = self.normalize_email(email)
user = self.model(email=email, 41 41 user = self.model(email=email,
is_staff=is_staff, is_active=True, 42 42 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 43 43 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 44 44 date_joined=now(), **extra_fields)
user.set_password(password) 45 45 user.set_password(password)
user.save(using=self._db) 46 46 user.save(using=self._db)
return user 47 47 return user
48 48
def create_user(self, email, password=None, **extra_fields): 49 49 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 50 50 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 51 51 body = '''
Visit the following link to confirm your email address: 52 52 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 53 53 https://flashy.cards/app/verifyemail/%s
54 54
If you did not register for Flashy, no action is required. 55 55 If you did not register for Flashy, no action is required.
''' 56 56 '''
57 57
assert send_mail("Flashy email verification", 58 58 assert send_mail("Flashy email verification",
body % user.confirmation_key, 59 59 body % user.confirmation_key,
"noreply@flashy.cards", 60 60 "noreply@flashy.cards",
[user.email]) 61 61 [user.email])
return user 62 62 return user
63 63
def create_superuser(self, email, password, **extra_fields): 64 64 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 65 65 return self._create_user(email, password, True, True, **extra_fields)
66 66
67 67
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 68 68 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 69 69 """
An extension of Django's default user model. 70 70 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 71 71 We use email as the username field, and include enrolled sections here
""" 72 72 """
objects = EmailOnlyUserManager() 73 73 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 74 74 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 75 75 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 76 76 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
confirmed_email = BooleanField(default=False) 77 77 confirmed_email = BooleanField(default=False)
78 78
def is_in_section(self, section): 79 79 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 80 80 return self.sections.filter(pk=section.pk).exists()
81 81
def pull(self, flashcard): 82 82 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 83 83 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 84 84 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 85 85 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 86 86 user_card.pulled = now()
user_card.save() 87 87 user_card.save()
import flashcards.notifications 88 88 import flashcards.notifications
89 89
flashcards.notifications.notify_score_change(flashcard) 90 90 flashcards.notifications.notify_score_change(flashcard)
flashcards.notifications.deck_card_score_change(flashcard) 91 91 flashcards.notifications.notify_pull(flashcard)
92 92
def unpull(self, flashcard): 93 93 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 94 94 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 95 95 raise ValueError("User not in the section this flashcard belongs to")
96 96
try: 97 97 try:
import flashcards.notifications 98 98 import flashcards.notifications
99 99
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 100 100 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 101 101 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 102 102 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 103 103 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 104 104 raise ValueError('Cannot unpull card that is not pulled.')
105 105
def get_deck(self, section): 106 106 def get_deck(self, section):
if not self.is_in_section(section): 107 107 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 108 108 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 109 109 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
110 110
def request_password_reset(self): 111 111 def request_password_reset(self):
token = default_token_generator.make_token(self) 112 112 token = default_token_generator.make_token(self)
113 113
body = ''' 114 114 body = '''
Visit the following link to reset your password: 115 115 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 116 116 https://flashy.cards/app/resetpassword/%d/%s
117 117
If you did not request a password reset, no action is required. 118 118 If you did not request a password reset, no action is required.
''' 119 119 '''
120 120
send_mail("Flashy password reset", 121 121 send_mail("Flashy password reset",
body % (self.pk, token), 122 122 body % (self.pk, token),
"noreply@flashy.cards", 123 123 "noreply@flashy.cards",
[self.email]) 124 124 [self.email])
125 125
def confirm_email(self, confirmation_key): 126 126 def confirm_email(self, confirmation_key):
# This will raise an exception if the email address is invalid 127 127 # This will raise an exception if the email address is invalid
self.email_address_set.confirm(confirmation_key, save=True) 128 128 self.email_address_set.confirm(confirmation_key, save=True)
self.confirmed_email = True 129 129 self.confirmed_email = True
self.save() 130 130 self.save()
131 131
132 132
class UserFlashcard(Model): 133 133 class UserFlashcard(Model):
""" 134 134 """
Represents the relationship between a user and a flashcard by: 135 135 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 136 136 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 137 137 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 138 138 3. A user has a flashcard hidden from them
""" 139 139 """
user = ForeignKey('User') 140 140 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 141 141 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 142 142 help_text="The user-specific mask on the card")
pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card") 143 143 pulled = DateTimeField(auto_now_add=True, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 144 144 flashcard = ForeignKey('Flashcard')
145 145
def get_mask(self): 146 146 def get_mask(self):
if self.mask is None: 147 147 if self.mask is None:
return self.flashcard.mask 148 148 return self.flashcard.mask
return self.mask 149 149 return self.mask
150 150
class Meta: 151 151 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 152 152 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 153 153 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 154 154 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 155 155 # By default, order by most recently pulled
ordering = ['-pulled'] 156 156 ordering = ['-pulled']
157 157
158 158
class FlashcardHide(Model): 159 159 class FlashcardHide(Model):
""" 160 160 """
Represents the property of a flashcard being hidden by a user. 161 161 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 162 162 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 163 163 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. 164 164 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 165 165 """
user = ForeignKey('User') 166 166 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 167 167 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 168 168 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 169 169 hidden = DateTimeField(auto_now_add=True)
170 170
class Meta: 171 171 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 172 172 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 173 173 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 174 174 index_together = ["user", "flashcard"]
175 175
176 176
class Flashcard(Model): 177 177 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 178 178 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') 179 179 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") 180 180 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") 181 181 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 182 182 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 183 183 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 184 184 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 185 185 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 186 186 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 187 187 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 188 188 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
189 189
class Meta: 190 190 class Meta:
# By default, order by most recently pushed 191 191 # By default, order by most recently pushed
ordering = ['-pushed'] 192 192 ordering = ['-pushed']
193 193
def is_hidden_from(self, user): 194 194 def is_hidden_from(self, user):
""" 195 195 """
A card can be hidden globally, but if a user has the card in their deck, 196 196 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 197 197 this visibility overrides a global hide.
:param user: 198 198 :param user:
:return: Whether the card is hidden from the user. 199 199 :return: Whether the card is hidden from the user.
""" 200 200 """
if self.userflashcard_set.filter(user=user).exists(): return False 201 201 if self.userflashcard_set.filter(user=user).exists(): return False
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 202 202 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return False 203 203 return False
204 204
def hide_from(self, user, reason=None): 205 205 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 206 206 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 207 207 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 208 208 if not created:
raise ValidationError("The card has already been hidden.") 209 209 raise ValidationError("The card has already been hidden.")
obj.save() 210 210 obj.save()
211 211
def is_in_deck(self, user): 212 212 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 213 213 return self.userflashcard_set.filter(user=user).exists()
214 214
def add_to_deck(self, user): 215 215 def add_to_deck(self, user):
if not user.is_in_section(self.section): 216 216 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 217 217 raise PermissionDenied("You don't have the permission to add this card")
try: 218 218 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 219 219 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 220 220 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 221 221 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 222 222 user_flashcard.save()
return user_flashcard 223 223 return user_flashcard
224 224
def edit(self, user, new_data): 225 225 def edit(self, user, new_data):
""" 226 226 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 227 227 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. 228 228 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 229 229 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 230 230 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 231 231 """
232 232
# content_changed is True iff either material_date or text were changed 233 233 # content_changed is True iff either material_date or text were changed
content_changed = False 234 234 content_changed = False
# create_new is True iff the user editing this card is the author of this card 235 235 # 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 236 236 # and there are no other users with this card in their decks
create_new = user != self.author or \ 237 237 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 238 238 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 239 239 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 240 240 content_changed = True
self.material_date = new_data['material_date'] 241 241 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 242 242 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 243 243 content_changed = True
self.text = new_data['text'] 244 244 self.text = new_data['text']
if create_new and content_changed: 245 245 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 246 246 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 247 247 self.previous_id = self.pk
self.pk = None 248 248 self.pk = None
self.mask = new_data.get('mask', self.mask) 249 249 self.mask = new_data.get('mask', self.mask)
self.save() 250 250 self.save()
self.add_to_deck(user) 251 251 self.add_to_deck(user)
else: 252 252 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 253 253 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 254 254 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 255 255 user_card.save()
return self 256 256 return self
257 257
def report(self, user, reason=None): 258 258 def report(self, user, reason=None):
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 259 259 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 260 260 obj.reason = reason
obj.save() 261 261 obj.save()
262 262
@cached_property 263 263 @cached_property
def score(self): 264 264 def score(self):
def seconds_since_epoch(dt): 265 265 def seconds_since_epoch(dt):
from datetime import datetime 266 266 from datetime import datetime
epoch = make_aware(datetime.utcfromtimestamp(0)) 267 267 epoch = make_aware(datetime.utcfromtimestamp(0))
delta = dt - epoch 268 268 delta = dt - epoch
return delta.total_seconds() 269 269 return delta.total_seconds()
270 270
z = 0 271 271 z = 0
rate = 1.0 / 3600 272 272 rate = 1.0 / 3600
for vote in self.userflashcard_set.iterator(): 273 273 for vote in self.userflashcard_set.iterator():
t = seconds_since_epoch(vote.pulled) 274 274 t = seconds_since_epoch(vote.pulled)
u = max(z, rate * t) 275 275 u = max(z, rate * t)
v = min(z, rate * t) 276 276 v = min(z, rate * t)
z = u + log1p(exp(v - u)) 277 277 z = u + log1p(exp(v - u))
return z 278 278 return z
279 279
@classmethod 280 280 @classmethod
def cards_visible_to(cls, user): 281 281 def cards_visible_to(cls, user):
""" 282 282 """
:param user: 283 283 :param user:
:return: A queryset with all cards that should be visible to a user. 284 284 :return: A queryset with all cards that should be visible to a user.
""" 285 285 """
return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user) 286 286 return cls.objects.filter(Q(author__confirmed_email=True) | Q(author=user)
).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user)) 287 287 ).exclude(Q(is_hidden=True) | Q(flashcardhide__user=user))
288 288
289 289
class UserFlashcardQuiz(Model): 290 290 class UserFlashcardQuiz(Model):
""" 291 291 """
An event of a user being quizzed on a flashcard. 292 292 An event of a user being quizzed on a flashcard.
""" 293 293 """
user_flashcard = ForeignKey(UserFlashcard) 294 294 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 295 295 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 296 296 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, default=None, help_text="The user's response") 297 297 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") 298 298 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
299 299
def status(self): 300 300 def status(self):
""" 301 301 """
There are three stages of a quiz object: 302 302 There are three stages of a quiz object:
1. the user has been shown the card 303 303 1. the user has been shown the card
2. the user has answered the card 304 304 2. the user has answered the card
3. the user has self-evaluated their response's correctness 305 305 3. the user has self-evaluated their response's correctness
306 306
:return: string (evaluated, answered, viewed) 307 307 :return: string (evaluated, answered, viewed)
""" 308 308 """
if self.correct is not None: return "evaluated" 309 309 if self.correct is not None: return "evaluated"
if self.response: return "answered" 310 310 if self.response: return "answered"
return "viewed" 311 311 return "viewed"
312 312
313 313
class Section(Model): 314 314 class Section(Model):
""" 315 315 """
A UCSD course taught by an instructor during a quarter. 316 316 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 317 317 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 318 318 We index gratuitously to support autofill and because this is primarily read-only
""" 319 319 """
department = CharField(db_index=True, max_length=50) 320 320 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 321 321 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 322 322 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 323 323 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 324 324 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 325 325 quarter = CharField(db_index=True, max_length=4)
326 326
@classmethod 327 327 @classmethod
def search(cls, terms): 328 328 def search(cls, terms):
""" 329 329 """
Search all fields of all sections for a particular set of terms 330 330 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 331 331 A matching section must match at least one field on each term
:param terms:iterable 332 332 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 333 333 :return: Matching QuerySet ordered by department and course number
""" 334 334 """
final_q = Q() 335 335 final_q = Q()
for term in terms: 336 336 for term in terms:
q = Q(department__icontains=term) 337 337 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 338 338 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 339 339 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 340 340 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 341 341 q |= Q(instructor__icontains=term)
final_q &= q 342 342 final_q &= q
qs = cls.objects.filter(final_q) 343 343 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 344 344 # 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 345 345 # 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)"}) 346 346 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 347 347 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 348 348 return qs
349 349
@property 350 350 @property
def is_whitelisted(self): 351 351 def is_whitelisted(self):
""" 352 352 """
:return: whether a whitelist exists for this section 353 353 :return: whether a whitelist exists for this section
""" 354 354 """
return self.whitelist.exists() 355 355 return self.whitelist.exists()
356 356
def is_user_on_whitelist(self, user): 357 357 def is_user_on_whitelist(self, user):
""" 358 358 """
:return: whether the user is on the waitlist for this section 359 359 :return: whether the user is on the waitlist for this section
""" 360 360 """
return self.whitelist.filter(email=user.email).exists() 361 361 return self.whitelist.filter(email=user.email).exists()
362 362
def enroll(self, user): 363 363 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 364 364 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 365 365 raise ValidationError('User is already enrolled in this section')
flashcards/notifications.py View file @ f53a10a
import serializers 1 1 import serializers
from rest_framework.renderers import JSONRenderer 2 2 from rest_framework.renderers import JSONRenderer
from ws4redis.publisher import RedisPublisher 3 3 from ws4redis.publisher import RedisPublisher
from ws4redis.redis_store import RedisMessage 4 4 from ws4redis.redis_store import RedisMessage
from ws4redis.redis_store import SELF 5 5 from ws4redis.redis_store import SELF
6 6
7 7
def notify_score_change(flashcard): 8 8 def notify_score_change(flashcard):
redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True) 9 9 redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True)
ws_message = JSONRenderer().render( 10 10 ws_message = JSONRenderer().render(
{'event_type': 'score_change', 'new_score': flashcard.score, 'flashcard_id': flashcard.pk}) 11 11 {'event_type': 'score_change', 'new_score': flashcard.score, 'flashcard_id': flashcard.pk})
message = RedisMessage(ws_message) 12 12 message = RedisMessage(ws_message)
redis_publisher.publish_message(message) 13 13 redis_publisher.publish_message(message)
14 14
15 15
def notify_new_card(flashcard): 16 16 def notify_new_card(flashcard):
redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True) 17 17 redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True)
ws_message = JSONRenderer().render( 18 18 ws_message = JSONRenderer().render(
{'event_type': 'new_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data}) 19 19 {'event_type': 'new_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data})
message = RedisMessage(ws_message) 20 20 message = RedisMessage(ws_message)
redis_publisher.publish_message(message) 21 21 redis_publisher.publish_message(message)
22 22
# def tell all clients the score changed 23 23 def notify_pull(flashcard):
# you added a card 24
# 25
def deck_card_score_change(flashcard): 26
redis_publisher = RedisPublisher(facility='deck/%d' % flashcard.section_id, users=[SELF]) 27 24 redis_publisher = RedisPublisher(facility='deck/%d' % flashcard.section_id, users=[SELF])
ws_message = JSONRenderer().render( 28 25 ws_message = JSONRenderer().render(
flashy/settings.py View file @ f53a10a
# 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
from datetime import datetime 3 3 from datetime import datetime
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
AUTH_USER_MODEL = 'flashcards.User' 14 14 AUTH_USER_MODEL = 'flashcards.User'
REST_FRAMEWORK = { 15 15 REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination', 16 16 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 20 17 17 'PAGE_SIZE': 20
} 18 18 }
INSTALLED_APPS = [ 19 19 INSTALLED_APPS = [
'simple_email_confirmation', 20 20 'simple_email_confirmation',
'flashcards', 21 21 'flashcards',
'django.contrib.admin', 22 22 'django.contrib.admin',
'django.contrib.admindocs', 23 23 'django.contrib.admindocs',
'django.contrib.auth', 24 24 'django.contrib.auth',
'django.contrib.contenttypes', 25 25 'django.contrib.contenttypes',
'django.contrib.sessions', 26 26 'django.contrib.sessions',
'django.contrib.messages', 27 27 'django.contrib.messages',
'django.contrib.staticfiles', 28 28 'django.contrib.staticfiles',
'ws4redis', 29 29 'ws4redis',
30
'rest_framework_swagger', 30 31 'rest_framework_swagger',
'rest_framework', 31 32 'rest_framework',
33 'django_extensions',
] 32 34 ]
33 35
WEBSOCKET_URL = '/ws/' 34 36 WEBSOCKET_URL = '/ws/'
35 37
36 38
MIDDLEWARE_CLASSES = ( 37 39 MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware', 38 40 'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware', 39 41 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 40 42 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 41 43 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 42 44 'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 43 45 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 44 46 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware', 45 47 'django.middleware.security.SecurityMiddleware',
) 46 48 )
47 49
ROOT_URLCONF = 'flashy.urls' 48 50 ROOT_URLCONF = 'flashy.urls'
49 51
AUTHENTICATION_BACKENDS = ( 50 52 AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', 51 53 'django.contrib.auth.backends.ModelBackend',
) 52 54 )
53 55
TEMPLATES = [ 54 56 TEMPLATES = [
{ 55 57 {
'BACKEND': 'django.template.backends.django.DjangoTemplates', 56 58 'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': ['templates/'], 57 59 'DIRS': ['templates/'],
'APP_DIRS': True, 58 60 'APP_DIRS': True,
'OPTIONS': { 59 61 'OPTIONS': {
'context_processors': [ 60 62 'context_processors': [
'django.template.context_processors.debug', 61 63 'django.template.context_processors.debug',
'django.template.context_processors.request', 62 64 'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth', 63 65 'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages', 64 66 'django.contrib.messages.context_processors.messages',
'django.core.context_processors.static', 65 67 'django.core.context_processors.static',
'ws4redis.context_processors.default', 66 68 'ws4redis.context_processors.default',
], 67 69 ],
}, 68 70 },
}, 69 71 },
] 70 72 ]
71 73
WSGI_APPLICATION = 'ws4redis.django_runserver.application' 72 74 WSGI_APPLICATION = 'ws4redis.django_runserver.application'
73 75
DATABASES = { 74 76 DATABASES = {
'default': { 75 77 'default': {
'ENGINE': 'django.db.backends.sqlite3', 76 78 'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 77 79 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
} 78 80 }
} 79 81 }
80 82
if IN_PRODUCTION: 81 83 if IN_PRODUCTION:
DATABASES['default'] = { 82 84 DATABASES['default'] = {
'ENGINE': 'django.db.backends.postgresql_psycopg2', 83 85 'ENGINE': 'django.db.backends.postgresql_psycopg2',
'NAME': 'flashy', 84 86 'NAME': 'flashy',
'USER': 'flashy', 85 87 'USER': 'flashy',
'PASSWORD': os.environ['FLASHY_DB_PW'], 86 88 'PASSWORD': os.environ['FLASHY_DB_PW'],
'HOST': 'localhost', 87 89 'HOST': 'localhost',
'PORT': '', 88 90 'PORT': '',
} 89 91 }
90 92
LANGUAGE_CODE = 'en-us' 91 93 LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'America/Los_Angeles' 92 94 TIME_ZONE = 'America/Los_Angeles'
USE_I18N = True 93 95 USE_I18N = True
USE_L10N = True 94 96 USE_L10N = True
USE_TZ = True 95 97 USE_TZ = True
96 98
QUARTER_START = UTC.localize(datetime(2015, 3, 30)) 97 99 QUARTER_START = UTC.localize(datetime(2015, 3, 30))
QUARTER_END = UTC.localize(datetime(2015, 6, 12)) 98 100 QUARTER_END = UTC.localize(datetime(2015, 6, 12))
99 101
STATIC_URL = '/static/' 100 102 STATIC_URL = '/static/'
STATIC_ROOT = 'static' 101 103 STATIC_ROOT = 'static'
102 104
# Four settings just to be sure 103 105 # Four settings just to be sure
EMAIL_FROM = 'noreply@flashy.cards' 104 106 EMAIL_FROM = 'noreply@flashy.cards'
EMAIL_HOST_USER = 'noreply@flashy.cards' 105 107 EMAIL_HOST_USER = 'noreply@flashy.cards'
DEFAULT_FROM_EMAIL = 'noreply@flashy.cards' 106 108 DEFAULT_FROM_EMAIL = 'noreply@flashy.cards'
SERVER_EMAIL = 'noreply@flashy.cards' 107 109 SERVER_EMAIL = 'noreply@flashy.cards'
108 110
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 109 111 EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
110 112
if IN_PRODUCTION: 111 113 if IN_PRODUCTION:
INSTALLED_APPS.append('django_ses') 112 114 INSTALLED_APPS.append('django_ses')
AWS_SES_REGION_NAME = 'us-west-2' 113 115 AWS_SES_REGION_NAME = 'us-west-2'
AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com' 114 116 AWS_SES_REGION_ENDPOINT = 'email.us-west-2.amazonaws.com'
EMAIL_BACKEND = 'django_ses.SESBackend' 115 117 EMAIL_BACKEND = 'django_ses.SESBackend'
116 118
if IN_PRODUCTION: 117 119 if IN_PRODUCTION:
SESSION_COOKIE_SECURE = True 118 120 SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True 119 121 CSRF_COOKIE_SECURE = True
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') 120 122 SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
# are we secure yet? 121 123 # are we secure yet?
122 124
if IN_PRODUCTION: 123 125 if IN_PRODUCTION:
LOGGING = { 124 126 LOGGING = {
'version': 1, 125 127 'version': 1,
'disable_existing_loggers': False, 126 128 'disable_existing_loggers': False,
'handlers': { 127 129 'handlers': {
'file': { 128 130 'file': {
requirements.txt View file @ f53a10a
#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
15 django-extensions