Commit 7ac46127a21a39e4069f99d85e05908eee37ebab

Authored by Laura Hawkins
1 parent 3b2a69dc87
Exists in master

working on notifying when adding to a deck

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

flashcards/models.py View file @ 7ac4612
from django.contrib.auth.models import AbstractUser, UserManager 1 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.contrib.auth.tokens import default_token_generator 2 2 from django.contrib.auth.tokens import default_token_generator
from django.core.cache import cache 3 3 from django.core.cache import cache
from django.core.exceptions import ValidationError 4 4 from django.core.exceptions import ValidationError
from django.core.exceptions import PermissionDenied, SuspiciousOperation 5 5 from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.core.mail import send_mail 6 6 from django.core.mail import send_mail
from django.db import IntegrityError 7 7 from django.db import IntegrityError
from django.db.models import * 8 8 from django.db.models import *
from django.utils.timezone import now 9 9 from django.utils.timezone import now
10 10
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 11 11 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 12 12 from fields import MaskField
13 13
14 14
# Hack to fix AbstractUser before subclassing it 15 15 # Hack to fix AbstractUser before subclassing it
16 16
AbstractUser._meta.get_field('email')._unique = True 17 17 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 18 18 AbstractUser._meta.get_field('username')._unique = False
19 19
20 20
class EmailOnlyUserManager(UserManager): 21 21 class EmailOnlyUserManager(UserManager):
""" 22 22 """
A tiny extension of Django's UserManager which correctly creates users 23 23 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 24 24 without usernames (using emails instead).
""" 25 25 """
26 26
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 27 27 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 28 28 """
Creates and saves a User with the given email and password. 29 29 Creates and saves a User with the given email and password.
""" 30 30 """
email = self.normalize_email(email) 31 31 email = self.normalize_email(email)
user = self.model(email=email, 32 32 user = self.model(email=email,
is_staff=is_staff, is_active=True, 33 33 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 34 34 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 35 35 date_joined=now(), **extra_fields)
user.set_password(password) 36 36 user.set_password(password)
user.save(using=self._db) 37 37 user.save(using=self._db)
return user 38 38 return user
39 39
def create_user(self, email, password=None, **extra_fields): 40 40 def create_user(self, email, password=None, **extra_fields):
user = self._create_user(email, password, False, False, **extra_fields) 41 41 user = self._create_user(email, password, False, False, **extra_fields)
body = ''' 42 42 body = '''
Visit the following link to confirm your email address: 43 43 Visit the following link to confirm your email address:
https://flashy.cards/app/verifyemail/%s 44 44 https://flashy.cards/app/verifyemail/%s
45 45
If you did not register for Flashy, no action is required. 46 46 If you did not register for Flashy, no action is required.
''' 47 47 '''
48 48
assert send_mail("Flashy email verification", 49 49 assert send_mail("Flashy email verification",
body % user.confirmation_key, 50 50 body % user.confirmation_key,
"noreply@flashy.cards", 51 51 "noreply@flashy.cards",
[user.email]) 52 52 [user.email])
return user 53 53 return user
54 54
def create_superuser(self, email, password, **extra_fields): 55 55 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 56 56 return self._create_user(email, password, True, True, **extra_fields)
57 57
58 58
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 59 59 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 60 60 """
An extension of Django's default user model. 61 61 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 62 62 We use email as the username field, and include enrolled sections here
""" 63 63 """
objects = EmailOnlyUserManager() 64 64 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 65 65 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 66 66 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 67 67 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
68 68
def is_in_section(self, section): 69 69 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 70 70 return self.sections.filter(pk=section.pk).exists()
71 71
def pull(self, flashcard): 72 72 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 73 73 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 74 74 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 75 75 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = now() 76 76 user_card.pulled = now()
user_card.save() 77 77 user_card.save()
import flashcards.notifications 78 78 import flashcards.notifications
flashcards.notifications.notify_score_change(flashcard) 79 79 flashcards.notifications.notify_score_change(flashcard)
80 flashcards.notifications.deck_card_score_change(flashcard)
80 81
def unpull(self, flashcard): 81 82 def unpull(self, flashcard):
if not self.is_in_section(flashcard.section): 82 83 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 83 84 raise ValueError("User not in the section this flashcard belongs to")
84 85
try: 85 86 try:
import flashcards.notifications 86 87 import flashcards.notifications
user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard) 87 88 user_card = UserFlashcard.objects.get(user=self, flashcard=flashcard)
user_card.delete() 88 89 user_card.delete()
flashcards.notifications.notify_score_change(flashcard) 89 90 flashcards.notifications.notify_score_change(flashcard)
except UserFlashcard.DoesNotExist: 90 91 except UserFlashcard.DoesNotExist:
raise ValueError('Cannot unpull card that is not pulled.') 91 92 raise ValueError('Cannot unpull card that is not pulled.')
92 93
def get_deck(self, section): 93 94 def get_deck(self, section):
if not self.is_in_section(section): 94 95 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 95 96 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 96 97 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
97 98
def request_password_reset(self): 98 99 def request_password_reset(self):
token = default_token_generator.make_token(self) 99 100 token = default_token_generator.make_token(self)
100 101
body = ''' 101 102 body = '''
Visit the following link to reset your password: 102 103 Visit the following link to reset your password:
https://flashy.cards/app/resetpassword/%d/%s 103 104 https://flashy.cards/app/resetpassword/%d/%s
104 105
If you did not request a password reset, no action is required. 105 106 If you did not request a password reset, no action is required.
''' 106 107 '''
107 108
send_mail("Flashy password reset", 108 109 send_mail("Flashy password reset",
body % (self.pk, token), 109 110 body % (self.pk, token),
"noreply@flashy.cards", 110 111 "noreply@flashy.cards",
[self.email]) 111 112 [self.email])
112 113
113 114
class UserFlashcard(Model): 114 115 class UserFlashcard(Model):
""" 115 116 """
Represents the relationship between a user and a flashcard by: 116 117 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 117 118 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 118 119 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 119 120 3. A user has a flashcard hidden from them
""" 120 121 """
user = ForeignKey('User') 121 122 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 122 123 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 123 124 help_text="The user-specific mask on the card")
pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") 124 125 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 125 126 flashcard = ForeignKey('Flashcard')
126 127
class Meta: 127 128 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 128 129 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 129 130 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 130 131 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 131 132 # By default, order by most recently pulled
ordering = ['-pulled'] 132 133 ordering = ['-pulled']
133 134
134 135
class FlashcardHide(Model): 135 136 class FlashcardHide(Model):
""" 136 137 """
Represents the property of a flashcard being hidden by a user. 137 138 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 138 139 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 139 140 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. 140 141 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 141 142 """
user = ForeignKey('User') 142 143 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 143 144 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 144 145 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 145 146 hidden = DateTimeField(auto_now_add=True)
146 147
class Meta: 147 148 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 148 149 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 149 150 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 150 151 index_together = ["user", "flashcard"]
151 152
152 153
class Flashcard(Model): 153 154 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 154 155 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') 155 156 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") 156 157 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") 157 158 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, 158 159 previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists") 159 160 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 160 161 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 161 162 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255, default='', 162 163 hide_reason = CharField(blank=True, null=True, max_length=255, default='',
help_text="Reason for hiding this card") 163 164 help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 164 165 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
165 166
class Meta: 166 167 class Meta:
# By default, order by most recently pushed 167 168 # By default, order by most recently pushed
ordering = ['-pushed'] 168 169 ordering = ['-pushed']
169 170
def is_hidden_from(self, user): 170 171 def is_hidden_from(self, user):
""" 171 172 """
A card can be hidden globally, but if a user has the card in their deck, 172 173 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 173 174 this visibility overrides a global hide.
:param user: 174 175 :param user:
:return: Whether the card is hidden from the user. 175 176 :return: Whether the card is hidden from the user.
""" 176 177 """
if self.userflashcard_set.filter(user=user).exists(): return False 177 178 if self.userflashcard_set.filter(user=user).exists(): return False
if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True 178 179 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return False 179 180 return False
180 181
def hide_from(self, user, reason=None): 181 182 def hide_from(self, user, reason=None):
if self.is_in_deck(user): user.unpull(self) 182 183 if self.is_in_deck(user): user.unpull(self)
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None) 183 184 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self, reason=None)
if not created: 184 185 if not created:
raise ValidationError("The card has already been hidden.") 185 186 raise ValidationError("The card has already been hidden.")
obj.save() 186 187 obj.save()
187 188
def is_in_deck(self, user): 188 189 def is_in_deck(self, user):
return self.userflashcard_set.filter(user=user).exists() 189 190 return self.userflashcard_set.filter(user=user).exists()
190 191
def add_to_deck(self, user): 191 192 def add_to_deck(self, user):
if not user.is_in_section(self.section): 192 193 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to add this card") 193 194 raise PermissionDenied("You don't have the permission to add this card")
try: 194 195 try:
user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) 195 196 user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask)
except IntegrityError: 196 197 except IntegrityError:
raise SuspiciousOperation("The flashcard is already in the user's deck") 197 198 raise SuspiciousOperation("The flashcard is already in the user's deck")
user_flashcard.save() 198 199 user_flashcard.save()
return user_flashcard 199 200 return user_flashcard
200 201
def edit(self, user, new_data): 201 202 def edit(self, user, new_data):
""" 202 203 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 203 204 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. 204 205 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 205 206 :param user: The user editing this card.
:param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 206 207 :param new_data: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 207 208 """
208 209
# content_changed is True iff either material_date or text were changed 209 210 # content_changed is True iff either material_date or text were changed
content_changed = False 210 211 content_changed = False
# create_new is True iff the user editing this card is the author of this card 211 212 # 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 212 213 # and there are no other users with this card in their decks
create_new = user != self.author or \ 213 214 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 214 215 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
if 'material_date' in new_data and self.material_date != new_data['material_date']: 215 216 if 'material_date' in new_data and self.material_date != new_data['material_date']:
content_changed = True 216 217 content_changed = True
self.material_date = new_data['material_date'] 217 218 self.material_date = new_data['material_date']
if 'text' in new_data and self.text != new_data['text']: 218 219 if 'text' in new_data and self.text != new_data['text']:
content_changed = True 219 220 content_changed = True
self.text = new_data['text'] 220 221 self.text = new_data['text']
if create_new and content_changed: 221 222 if create_new and content_changed:
if self.is_in_deck(user): user.unpull(self) 222 223 if self.is_in_deck(user): user.unpull(self)
self.previous_id = self.pk 223 224 self.previous_id = self.pk
self.pk = None 224 225 self.pk = None
self.mask = new_data.get('mask', self.mask) 225 226 self.mask = new_data.get('mask', self.mask)
self.save() 226 227 self.save()
self.add_to_deck(user) 227 228 self.add_to_deck(user)
else: 228 229 else:
user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self) 229 230 user_card, created = UserFlashcard.objects.get_or_create(user=user, flashcard=self)
user_card.mask = new_data.get('mask', user_card.mask) 230 231 user_card.mask = new_data.get('mask', user_card.mask)
user_card.save() 231 232 user_card.save()
return self 232 233 return self
233 234
def report(self, user, reason=None): 234 235 def report(self, user, reason=None):
obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self) 235 236 obj, created = FlashcardHide.objects.get_or_create(user=user, flashcard=self)
obj.reason = reason 236 237 obj.reason = reason
obj.save() 237 238 obj.save()
238 239
@property 239 240 @property
def score(self): 240 241 def score(self):
return self.userflashcard_set.count() 241 242 return self.userflashcard_set.count()
242 243
@classmethod 243 244 @classmethod
def cards_visible_to(cls, user): 244 245 def cards_visible_to(cls, user):
""" 245 246 """
:param user: 246 247 :param user:
:return: A queryset with all cards that should be visible to a user. 247 248 :return: A queryset with all cards that should be visible to a user.
""" 248 249 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 249 250 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
250 251
251 252
class UserFlashcardQuiz(Model): 252 253 class UserFlashcardQuiz(Model):
""" 253 254 """
An event of a user being quizzed on a flashcard. 254 255 An event of a user being quizzed on a flashcard.
""" 255 256 """
user_flashcard = ForeignKey(UserFlashcard) 256 257 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 257 258 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 258 259 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") 259 260 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") 260 261 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
261 262
def status(self): 262 263 def status(self):
""" 263 264 """
There are three stages of a quiz object: 264 265 There are three stages of a quiz object:
1. the user has been shown the card 265 266 1. the user has been shown the card
2. the user has answered the card 266 267 2. the user has answered the card
3. the user has self-evaluated their response's correctness 267 268 3. the user has self-evaluated their response's correctness
268 269
:return: string (evaluated, answered, viewed) 269 270 :return: string (evaluated, answered, viewed)
""" 270 271 """
if self.correct is not None: return "evaluated" 271 272 if self.correct is not None: return "evaluated"
if self.response: return "answered" 272 273 if self.response: return "answered"
return "viewed" 273 274 return "viewed"
274 275
275 276
class Section(Model): 276 277 class Section(Model):
""" 277 278 """
A UCSD course taught by an instructor during a quarter. 278 279 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 279 280 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 280 281 We index gratuitously to support autofill and because this is primarily read-only
""" 281 282 """
department = CharField(db_index=True, max_length=50) 282 283 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 283 284 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 284 285 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 285 286 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 286 287 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 287 288 quarter = CharField(db_index=True, max_length=4)
288 289
@classmethod 289 290 @classmethod
def search(cls, terms): 290 291 def search(cls, terms):
""" 291 292 """
Search all fields of all sections for a particular set of terms 292 293 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 293 294 A matching section must match at least one field on each term
:param terms:iterable 294 295 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 295 296 :return: Matching QuerySet ordered by department and course number
""" 296 297 """
final_q = Q() 297 298 final_q = Q()
for term in terms: 298 299 for term in terms:
q = Q(department__icontains=term) 299 300 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 300 301 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 301 302 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 302 303 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 303 304 q |= Q(instructor__icontains=term)
final_q &= q 304 305 final_q &= q
qs = cls.objects.filter(final_q) 305 306 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 306 307 # 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 307 308 # 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)"}) 308 309 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 309 310 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 310 311 return qs
311 312
@property 312 313 @property
def is_whitelisted(self): 313 314 def is_whitelisted(self):
""" 314 315 """
:return: whether a whitelist exists for this section 315 316 :return: whether a whitelist exists for this section
""" 316 317 """
return self.whitelist.exists() 317 318 return self.whitelist.exists()
318 319
def is_user_on_whitelist(self, user): 319 320 def is_user_on_whitelist(self, user):
""" 320 321 """
:return: whether the user is on the waitlist for this section 321 322 :return: whether the user is on the waitlist for this section
""" 322 323 """
return self.whitelist.filter(email=user.email).exists() 323 324 return self.whitelist.filter(email=user.email).exists()
324 325
def enroll(self, user): 325 326 def enroll(self, user):
if user.sections.filter(pk=self.pk).exists(): 326 327 if user.sections.filter(pk=self.pk).exists():
raise ValidationError('User is already enrolled in this section') 327 328 raise ValidationError('User is already enrolled in this section')
if self.is_whitelisted and not self.is_user_on_whitelist(user): 328 329 if self.is_whitelisted and not self.is_user_on_whitelist(user):
raise PermissionDenied("User must be on the whitelist to add this section.") 329 330 raise PermissionDenied("User must be on the whitelist to add this section.")
self.user_set.add(user) 330 331 self.user_set.add(user)
331 332
def drop(self, user): 332 333 def drop(self, user):
if not user.sections.filter(pk=self.pk).exists(): 333 334 if not user.sections.filter(pk=self.pk).exists():
raise ValidationError("User is not enrolled in the section.") 334 335 raise ValidationError("User is not enrolled in the section.")
self.user_set.remove(user) 335 336 self.user_set.remove(user)
336 337
class Meta: 337 338 class Meta:
flashcards/notifications.py View file @ 7ac4612
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
5 from ws4redis.redis_store import SELF
5 6
6 7
def notify_score_change(flashcard): 7 8 def notify_score_change(flashcard):
redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True) 8 9 redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True)
ws_message = JSONRenderer().render( 9 10 ws_message = JSONRenderer().render(
{'event_type': 'score_change', 'new_score': flashcard.score, 'flashcard_id': flashcard.pk}) 10 11 {'event_type': 'score_change', 'new_score': flashcard.score, 'flashcard_id': flashcard.pk})
message = RedisMessage(ws_message) 11 12 message = RedisMessage(ws_message)
redis_publisher.publish_message(message) 12 13 redis_publisher.publish_message(message)
13 14
14 15
def notify_new_card(flashcard): 15 16 def notify_new_card(flashcard):
redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True) 16 17 redis_publisher = RedisPublisher(facility='feed/%d' % flashcard.section_id, broadcast=True)
ws_message = JSONRenderer().render( 17 18 ws_message = JSONRenderer().render(
{'event_type': 'new_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data}) 18 19 {'event_type': 'new_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data})
message = RedisMessage(ws_message) 19 20 message = RedisMessage(ws_message)
redis_publisher.publish_message(message) 20 21 redis_publisher.publish_message(message)
22
23 # def tell all clients the score changed
24 # you added a card
25 #
26 def deck_card_score_change(flashcard):
27 redis_publisher = RedisPublisher(facility='deck/%d' % flashcard.section_id, users=[SELF])
28 ws_message = JSONRenderer().render(
29 {'event_type': 'pull_card', 'flashcard': serializers.FlashcardSerializer(flashcard).data})
30 message = RedisMessage(ws_message)
31 redis_publisher.publish_message(message)
flashcards/views.py View file @ 7ac4612
from random import sample 1 1 from random import sample
2 2
import django 3 3 import django
from django.contrib import auth 4 4 from django.contrib import auth
from django.shortcuts import get_object_or_404 5 5 from django.shortcuts import get_object_or_404
from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer 6 6 from flashcards.api import StandardResultsSetPagination, IsEnrolledInAssociatedSection, IsFlashcardReviewer
from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz 7 7 from flashcards.models import Section, User, Flashcard, FlashcardHide, UserFlashcardQuiz
from flashcards.notifications import notify_new_card 8 8 from flashcards.notifications import notify_new_card
from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \ 9 9 from flashcards.serializers import SectionSerializer, UserUpdateSerializer, RegistrationSerializer, UserSerializer, \
PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \ 10 10 PasswordResetSerializer, PasswordResetRequestSerializer, EmailPasswordSerializer, FlashcardSerializer, \
FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \ 11 11 FlashcardUpdateSerializer, QuizRequestSerializer, QuizResponseSerializer, \
QuizAnswerRequestSerializer, DeepSectionSerializer 12 12 QuizAnswerRequestSerializer, DeepSectionSerializer
from rest_framework.decorators import detail_route, permission_classes, api_view, list_route 13 13 from rest_framework.decorators import detail_route, permission_classes, api_view, list_route
from rest_framework.generics import ListAPIView, GenericAPIView 14 14 from rest_framework.generics import ListAPIView, GenericAPIView
from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin 15 15 from rest_framework.mixins import CreateModelMixin, RetrieveModelMixin, UpdateModelMixin
from rest_framework.permissions import IsAuthenticated 16 16 from rest_framework.permissions import IsAuthenticated
from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet 17 17 from rest_framework.viewsets import ReadOnlyModelViewSet, GenericViewSet
from django.core.mail import send_mail 18 18 from django.core.mail import send_mail
from django.contrib.auth import authenticate 19 19 from django.contrib.auth import authenticate
from django.contrib.auth.tokens import default_token_generator 20 20 from django.contrib.auth.tokens import default_token_generator
from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK 21 21 from rest_framework.status import HTTP_204_NO_CONTENT, HTTP_201_CREATED, HTTP_200_OK
from rest_framework.response import Response 22 22 from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied 23 23 from rest_framework.exceptions import AuthenticationFailed, NotAuthenticated, ValidationError, PermissionDenied
from simple_email_confirmation import EmailAddress 24 24 from simple_email_confirmation import EmailAddress
25 25
26 26
class SectionViewSet(ReadOnlyModelViewSet): 27 27 class SectionViewSet(ReadOnlyModelViewSet):
queryset = Section.objects.all() 28 28 queryset = Section.objects.all()
serializer_class = DeepSectionSerializer 29 29 serializer_class = DeepSectionSerializer
pagination_class = StandardResultsSetPagination 30 30 pagination_class = StandardResultsSetPagination
permission_classes = [IsAuthenticated] 31 31 permission_classes = [IsAuthenticated]
32 32
@detail_route(methods=['GET']) 33 33 @detail_route(methods=['GET'])
def flashcards(self, request, pk): 34 34 def flashcards(self, request, pk):
""" 35 35 """
Gets flashcards for a section, excluding hidden cards. 36 36 Gets flashcards for a section, excluding hidden cards.
Returned in strictly chronological order (material date). 37 37 Returned in strictly chronological order (material date).
""" 38 38 """
flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all() 39 39 flashcards = Flashcard.cards_visible_to(request.user).filter(section=self.get_object()).all()
return Response(FlashcardSerializer(flashcards, many=True).data) 40 40 return Response(FlashcardSerializer(flashcards, many=True).data)
41 41
@detail_route(methods=['POST']) 42 42 @detail_route(methods=['POST'])
def enroll(self, request, pk): 43 43 def enroll(self, request, pk):
""" 44 44 """
Add the current user to a specified section 45 45 Add the current user to a specified section
If the class has a whitelist, but the user is not on the whitelist, the request will fail. 46 46 If the class has a whitelist, but the user is not on the whitelist, the request will fail.
--- 47 47 ---
view_mocker: flashcards.api.mock_no_params 48 48 view_mocker: flashcards.api.mock_no_params
""" 49 49 """
50 50
self.get_object().enroll(request.user) 51 51 self.get_object().enroll(request.user)
return Response(status=HTTP_204_NO_CONTENT) 52 52 return Response(status=HTTP_204_NO_CONTENT)
53 53
@detail_route(methods=['POST']) 54 54 @detail_route(methods=['POST'])
def drop(self, request, pk): 55 55 def drop(self, request, pk):
""" 56 56 """
Remove the current user from a specified section 57 57 Remove the current user from a specified section
If the user is not in the class, the request will fail. 58 58 If the user is not in the class, the request will fail.
--- 59 59 ---
view_mocker: flashcards.api.mock_no_params 60 60 view_mocker: flashcards.api.mock_no_params
""" 61 61 """
try: 62 62 try:
self.get_object().drop(request.user) 63 63 self.get_object().drop(request.user)
except django.core.exceptions.PermissionDenied as e: 64 64 except django.core.exceptions.PermissionDenied as e:
raise PermissionDenied(e) 65 65 raise PermissionDenied(e)
except django.core.exceptions.ValidationError as e: 66 66 except django.core.exceptions.ValidationError as e:
raise ValidationError(e) 67 67 raise ValidationError(e)
return Response(status=HTTP_204_NO_CONTENT) 68 68 return Response(status=HTTP_204_NO_CONTENT)
69 69
@list_route(methods=['GET']) 70 70 @list_route(methods=['GET'])
def search(self, request): 71 71 def search(self, request):
""" 72 72 """
Returns a list of sections which match a user's query 73 73 Returns a list of sections which match a user's query
--- 74 74 ---
parameters: 75 75 parameters:
- name: q 76 76 - name: q
description: space-separated list of terms 77 77 description: space-separated list of terms
required: true 78 78 required: true
type: form 79 79 type: form
response_serializer: SectionSerializer 80 80 response_serializer: SectionSerializer
""" 81 81 """
query = request.GET.get('q', None) 82 82 query = request.GET.get('q', None)
if not query: return Response('[]') 83 83 if not query: return Response('[]')
qs = Section.search(query.split(' '))[:20] 84 84 qs = Section.search(query.split(' '))[:20]
data = SectionSerializer(qs, many=True).data 85 85 data = SectionSerializer(qs, many=True).data
return Response(data) 86 86 return Response(data)
87 87
@detail_route(methods=['GET']) 88 88 @detail_route(methods=['GET'])
def deck(self, request, pk): 89 89 def deck(self, request, pk):
""" 90 90 """
Gets the contents of a user's deck for a given section. 91 91 Gets the contents of a user's deck for a given section.
""" 92 92 """
qs = request.user.get_deck(self.get_object()) 93 93 qs = request.user.get_deck(self.get_object())
serializer = FlashcardSerializer(qs, many=True) 94 94 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 95 95 return Response(serializer.data)
96 96
@detail_route(methods=['GET'], permission_classes=[IsAuthenticated]) 97 97 @detail_route(methods=['GET'], permission_classes=[IsAuthenticated])
def ordered_deck(self, request, pk): 98 98 def ordered_deck(self, request, pk):
""" 99 99 """
Get a chronological order by material_date of flashcards for a section. 100 100 Get a chronological order by material_date of flashcards for a section.
This excludes hidden card. 101 101 This excludes hidden card.
""" 102 102 """
qs = request.user.get_deck(self.get_object()).order_by('-material_date') 103 103 qs = request.user.get_deck(self.get_object()).order_by('-material_date')
serializer = FlashcardSerializer(qs, many=True) 104 104 serializer = FlashcardSerializer(qs, many=True)
return Response(serializer.data) 105 105 return Response(serializer.data)
106 106
@detail_route(methods=['GET']) 107 107 @detail_route(methods=['GET'])
def feed(self, request, pk): 108 108 def feed(self, request, pk):
""" 109 109 """
Gets the contents of a user's feed for a section. 110 110 Gets the contents of a user's feed for a section.
Exclude cards that are already in the user's deck 111 111 Exclude cards that are already in the user's deck
""" 112 112 """
serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True) 113 113 serializer = FlashcardSerializer(self.get_object().get_feed_for_user(request.user), many=True)
return Response(serializer.data) 114 114 return Response(serializer.data)
115 115
116 116
class UserSectionListView(ListAPIView): 117 117 class UserSectionListView(ListAPIView):
serializer_class = DeepSectionSerializer 118 118 serializer_class = DeepSectionSerializer
permission_classes = [IsAuthenticated] 119 119 permission_classes = [IsAuthenticated]
120 120
def get_queryset(self): 121 121 def get_queryset(self):
return self.request.user.sections.all() 122 122 return self.request.user.sections.all()
123 123
def paginate_queryset(self, queryset): return None 124 124 def paginate_queryset(self, queryset): return None
125 125
126 126
class UserDetail(GenericAPIView): 127 127 class UserDetail(GenericAPIView):
serializer_class = UserSerializer 128 128 serializer_class = UserSerializer
permission_classes = [IsAuthenticated] 129 129 permission_classes = [IsAuthenticated]
130 130
def patch(self, request, format=None): 131 131 def patch(self, request, format=None):
""" 132 132 """
Updates the user's password, or verifies their email address 133 133 Updates the user's password, or verifies their email address
--- 134 134 ---
request_serializer: UserUpdateSerializer 135 135 request_serializer: UserUpdateSerializer
response_serializer: UserSerializer 136 136 response_serializer: UserSerializer
""" 137 137 """
data = UserUpdateSerializer(data=request.data, context={'user': request.user}) 138 138 data = UserUpdateSerializer(data=request.data, context={'user': request.user})
data.is_valid(raise_exception=True) 139 139 data.is_valid(raise_exception=True)
data = data.validated_data 140 140 data = data.validated_data
141 141
if 'new_password' in data: 142 142 if 'new_password' in data:
if not request.user.check_password(data['old_password']): 143 143 if not request.user.check_password(data['old_password']):
raise ValidationError('old_password is incorrect') 144 144 raise ValidationError('old_password is incorrect')
request.user.set_password(data['new_password']) 145 145 request.user.set_password(data['new_password'])
request.user.save() 146 146 request.user.save()
147 147
if 'confirmation_key' in data: 148 148 if 'confirmation_key' in data:
try: 149 149 try:
request.user.confirm_email(data['confirmation_key']) 150 150 request.user.confirm_email(data['confirmation_key'])
except EmailAddress.DoesNotExist: 151 151 except EmailAddress.DoesNotExist:
raise ValidationError('confirmation_key is invalid') 152 152 raise ValidationError('confirmation_key is invalid')
153 153
return Response(UserSerializer(request.user).data) 154 154 return Response(UserSerializer(request.user).data)
155 155
def get(self, request, format=None): 156 156 def get(self, request, format=None):
""" 157 157 """
Return data about the user 158 158 Return data about the user
--- 159 159 ---
response_serializer: UserSerializer 160 160 response_serializer: UserSerializer
""" 161 161 """
serializer = UserSerializer(request.user, context={'request': request}) 162 162 serializer = UserSerializer(request.user, context={'request': request})
return Response(serializer.data) 163 163 return Response(serializer.data)
164 164
def delete(self, request): 165 165 def delete(self, request):
""" 166 166 """
Irrevocably delete the user and their data 167 167 Irrevocably delete the user and their data
168 168
Yes, really 169 169 Yes, really
""" 170 170 """
request.user.delete() 171 171 request.user.delete()
return Response(status=HTTP_204_NO_CONTENT) 172 172 return Response(status=HTTP_204_NO_CONTENT)
173 173
174 174
@api_view(['POST']) 175 175 @api_view(['POST'])
def register(request, format=None): 176 176 def register(request, format=None):
""" 177 177 """
Register a new user 178 178 Register a new user
--- 179 179 ---
request_serializer: EmailPasswordSerializer 180 180 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 181 181 response_serializer: UserSerializer
""" 182 182 """
data = RegistrationSerializer(data=request.data) 183 183 data = RegistrationSerializer(data=request.data)
data.is_valid(raise_exception=True) 184 184 data.is_valid(raise_exception=True)
185 185
User.objects.create_user(**data.validated_data) 186 186 User.objects.create_user(**data.validated_data)
user = authenticate(**data.validated_data) 187 187 user = authenticate(**data.validated_data)
auth.login(request, user) 188 188 auth.login(request, user)
189 189
return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED) 190 190 return Response(UserSerializer(request.user).data, status=HTTP_201_CREATED)
191 191
192 192
@api_view(['POST']) 193 193 @api_view(['POST'])
def login(request): 194 194 def login(request):
""" 195 195 """
Authenticates user and returns user data if valid. 196 196 Authenticates user and returns user data if valid.
--- 197 197 ---
request_serializer: EmailPasswordSerializer 198 198 request_serializer: EmailPasswordSerializer
response_serializer: UserSerializer 199 199 response_serializer: UserSerializer
""" 200 200 """
201 201
data = EmailPasswordSerializer(data=request.data) 202 202 data = EmailPasswordSerializer(data=request.data)
data.is_valid(raise_exception=True) 203 203 data.is_valid(raise_exception=True)
user = authenticate(**data.validated_data) 204 204 user = authenticate(**data.validated_data)
205 205
if user is None: 206 206 if user is None:
raise AuthenticationFailed('Invalid email or password') 207 207 raise AuthenticationFailed('Invalid email or password')
if not user.is_active: 208 208 if not user.is_active:
raise NotAuthenticated('Account is disabled') 209 209 raise NotAuthenticated('Account is disabled')
auth.login(request, user) 210 210 auth.login(request, user)
return Response(UserSerializer(request.user).data) 211 211 return Response(UserSerializer(request.user).data)
212 212
213 213
@api_view(['POST']) 214 214 @api_view(['POST'])
@permission_classes((IsAuthenticated,)) 215 215 @permission_classes((IsAuthenticated,))
def logout(request, format=None): 216 216 def logout(request, format=None):
""" 217 217 """
Logs the authenticated user out. 218 218 Logs the authenticated user out.
""" 219 219 """
auth.logout(request) 220 220 auth.logout(request)
return Response(status=HTTP_204_NO_CONTENT) 221 221 return Response(status=HTTP_204_NO_CONTENT)
222 222
223 223
@api_view(['POST']) 224 224 @api_view(['POST'])
def request_password_reset(request, format=None): 225 225 def request_password_reset(request, format=None):
""" 226 226 """
Send a password reset token/link to the provided email. 227 227 Send a password reset token/link to the provided email.
--- 228 228 ---
request_serializer: PasswordResetRequestSerializer 229 229 request_serializer: PasswordResetRequestSerializer
""" 230 230 """
data = PasswordResetRequestSerializer(data=request.data) 231 231 data = PasswordResetRequestSerializer(data=request.data)
data.is_valid(raise_exception=True) 232 232 data.is_valid(raise_exception=True)
get_object_or_404(User, email=data['email'].value).request_password_reset() 233 233 get_object_or_404(User, email=data['email'].value).request_password_reset()
return Response(status=HTTP_204_NO_CONTENT) 234 234 return Response(status=HTTP_204_NO_CONTENT)
235 235
236 236
@api_view(['POST']) 237 237 @api_view(['POST'])
def reset_password(request, format=None): 238 238 def reset_password(request, format=None):
""" 239 239 """
Updates user's password to new password if token is valid. 240 240 Updates user's password to new password if token is valid.
--- 241 241 ---
request_serializer: PasswordResetSerializer 242 242 request_serializer: PasswordResetSerializer
""" 243 243 """
data = PasswordResetSerializer(data=request.data) 244 244 data = PasswordResetSerializer(data=request.data)
data.is_valid(raise_exception=True) 245 245 data.is_valid(raise_exception=True)
246 246
user = User.objects.get(id=data['uid'].value) 247 247 user = User.objects.get(id=data['uid'].value)
# Check token validity. 248 248 # Check token validity.
249 249
if default_token_generator.check_token(user, data['token'].value): 250 250 if default_token_generator.check_token(user, data['token'].value):
user.set_password(data['new_password'].value) 251 251 user.set_password(data['new_password'].value)
user.save() 252 252 user.save()
else: 253 253 else:
raise ValidationError('Could not verify reset token') 254 254 raise ValidationError('Could not verify reset token')
return Response(status=HTTP_204_NO_CONTENT) 255 255 return Response(status=HTTP_204_NO_CONTENT)
256 256
257 257
class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin): 258 258 class FlashcardViewSet(GenericViewSet, CreateModelMixin, RetrieveModelMixin):
queryset = Flashcard.objects.all() 259 259 queryset = Flashcard.objects.all()
serializer_class = FlashcardSerializer 260 260 serializer_class = FlashcardSerializer
permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection] 261 261 permission_classes = [IsAuthenticated, IsEnrolledInAssociatedSection]
262 262
# Override create in CreateModelMixin 263 263 # Override create in CreateModelMixin
def create(self, request, *args, **kwargs): 264 264 def create(self, request, *args, **kwargs):
serializer = FlashcardSerializer(data=request.data) 265 265 serializer = FlashcardSerializer(data=request.data)
serializer.is_valid(raise_exception=True) 266 266 serializer.is_valid(raise_exception=True)
data = serializer.validated_data 267 267 data = serializer.validated_data
if not request.user.is_in_section(data['section']): 268 268 if not request.user.is_in_section(data['section']):
raise PermissionDenied('The user is not enrolled in that section') 269 269 raise PermissionDenied('The user is not enrolled in that section')
data['author'] = request.user 270 270 data['author'] = request.user
flashcard = Flashcard.objects.create(**data) 271 271 flashcard = Flashcard.objects.create(**data)
self.perform_create(flashcard) 272 272 self.perform_create(flashcard)
notify_new_card(flashcard) 273 273 notify_new_card(flashcard)
headers = self.get_success_headers(data) 274 274 headers = self.get_success_headers(data)
response_data = FlashcardSerializer(flashcard).data 275 275 response_data = FlashcardSerializer(flashcard).data
276 276
return Response(response_data, status=HTTP_201_CREATED, headers=headers) 277 277 return Response(response_data, status=HTTP_201_CREATED, headers=headers)
278 278
@detail_route(methods=['POST']) 279 279 @detail_route(methods=['POST'])
def unhide(self, request, pk): 280 280 def unhide(self, request, pk):
""" 281 281 """
Unhide the given card 282 282 Unhide the given card
--- 283 283 ---
view_mocker: flashcards.api.mock_no_params 284 284 view_mocker: flashcards.api.mock_no_params
""" 285 285 """
hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object()) 286 286 hide = get_object_or_404(FlashcardHide, user=request.user, flashcard=self.get_object())
hide.delete() 287 287 hide.delete()
return Response(status=HTTP_204_NO_CONTENT) 288 288 return Response(status=HTTP_204_NO_CONTENT)
289 289
@detail_route(methods=['POST']) 290 290 @detail_route(methods=['POST'])
def report(self, request, pk): 291 291 def report(self, request, pk):
""" 292 292 """
Hide the given card 293 293 Hide the given card
--- 294 294 ---
view_mocker: flashcards.api.mock_no_params 295 295 view_mocker: flashcards.api.mock_no_params
""" 296 296 """
self.get_object().report(request.user) 297 297 self.get_object().report(request.user)
return Response(status=HTTP_204_NO_CONTENT) 298 298 return Response(status=HTTP_204_NO_CONTENT)
299 299
hide = report 300 300 hide = report
301 301
@detail_route(methods=['POST']) 302 302 @detail_route(methods=['POST'])
def pull(self, request, pk): 303 303 def pull(self, request, pk):
""" 304 304 """
Pull a card from the live feed into the user's deck. 305 305 Pull a card from the live feed into the user's deck.
--- 306 306 ---
view_mocker: flashcards.api.mock_no_params 307 307 view_mocker: flashcards.api.mock_no_params
""" 308 308 """
flashcard = self.get_object() 309 309
user.unpull(flashcard) 310 310 request.user.pull(self.get_object())
return Response(status=HTTP_204_NO_CONTENT) 311 311 return Response(status=HTTP_204_NO_CONTENT)
312 312
313 313
@detail_route(methods=['POST']) 314 314 @detail_route(methods=['POST'])
def unpull(self, request, pk): 315 315 def unpull(self, request, pk):
""" 316 316 """
Unpull a card from the user's deck 317 317 Unpull a card from the user's deck
--- 318 318 ---
view_mocker: flashcards.api.mock_no_params 319 319 view_mocker: flashcards.api.mock_no_params
scripts/run_local.sh View file @ 7ac4612
#!/bin/bash -xe 1 1 #!/bin/bash -xe
2 2
if [ ! -d "../flashy-frontend/" ]; then 3 3 if [ ! -d "../flashy-frontend/" ]; then
echo "In order to serve the frontend, flashy-frontend must be in the parent dir" 4 4 echo "In order to serve the frontend, flashy-frontend must be in the parent dir"
exit 1 5 5 exit 1
fi 6 6 fi
7 7
if [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then 8 8 if [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
source venv/bin/activate 9 9 source venv/bin/activate
elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then 10 10 elif [ "$(expr substr $(uname -s) 1 10)" == "MINGW32_NT" ]; then
source venv/Scripts/activate 11 11 source venv/Scripts/activate
fi 12 12 fi