Commit 106a612821898f93c98797779eb92336c2d483e0

Authored by Andrew Buss
1 parent 6e9adabd2b
Exists in master

fixed is_hidden_from

Showing 1 changed file with 3 additions and 5 deletions Inline Diff

flashcards/models.py View file @ 106a612
from datetime import datetime 1
2
from django.contrib.auth.models import AbstractUser, UserManager 3 1 from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied 4 2 from django.core.exceptions import PermissionDenied
from django.db.models import * 5 3 from django.db.models import *
from django.utils.timezone import now 6 4 from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin 7 5 from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField 8 6 from fields import MaskField
9 7
10 8
# Hack to fix AbstractUser before subclassing it 11 9 # Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True 12 10 AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False 13 11 AbstractUser._meta.get_field('username')._unique = False
14 12
15 13
class EmailOnlyUserManager(UserManager): 16 14 class EmailOnlyUserManager(UserManager):
""" 17 15 """
A tiny extension of Django's UserManager which correctly creates users 18 16 A tiny extension of Django's UserManager which correctly creates users
without usernames (using emails instead). 19 17 without usernames (using emails instead).
""" 20 18 """
21 19
def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): 22 20 def _create_user(self, email, password, is_staff, is_superuser, **extra_fields):
""" 23 21 """
Creates and saves a User with the given email and password. 24 22 Creates and saves a User with the given email and password.
""" 25 23 """
email = self.normalize_email(email) 26 24 email = self.normalize_email(email)
user = self.model(email=email, 27 25 user = self.model(email=email,
is_staff=is_staff, is_active=True, 28 26 is_staff=is_staff, is_active=True,
is_superuser=is_superuser, 29 27 is_superuser=is_superuser,
date_joined=now(), **extra_fields) 30 28 date_joined=now(), **extra_fields)
user.set_password(password) 31 29 user.set_password(password)
user.save(using=self._db) 32 30 user.save(using=self._db)
return user 33 31 return user
34 32
def create_user(self, email, password=None, **extra_fields): 35 33 def create_user(self, email, password=None, **extra_fields):
return self._create_user(email, password, False, False, **extra_fields) 36 34 return self._create_user(email, password, False, False, **extra_fields)
37 35
def create_superuser(self, email, password, **extra_fields): 38 36 def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields) 39 37 return self._create_user(email, password, True, True, **extra_fields)
40 38
41 39
class User(AbstractUser, SimpleEmailConfirmationUserMixin): 42 40 class User(AbstractUser, SimpleEmailConfirmationUserMixin):
""" 43 41 """
An extension of Django's default user model. 44 42 An extension of Django's default user model.
We use email as the username field, and include enrolled sections here 45 43 We use email as the username field, and include enrolled sections here
""" 46 44 """
objects = EmailOnlyUserManager() 47 45 objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email' 48 46 USERNAME_FIELD = 'email'
REQUIRED_FIELDS = [] 49 47 REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") 50 48 sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
51 49
def is_in_section(self, section): 52 50 def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists() 53 51 return self.sections.filter(pk=section.pk).exists()
54 52
def pull(self, flashcard): 55 53 def pull(self, flashcard):
if not self.is_in_section(flashcard.section): 56 54 if not self.is_in_section(flashcard.section):
raise ValueError("User not in the section this flashcard belongs to") 57 55 raise ValueError("User not in the section this flashcard belongs to")
user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) 58 56 user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard)
user_card.pulled = datetime.now() 59 57 user_card.pulled = datetime.now()
user_card.save() 60 58 user_card.save()
61 59
def get_deck(self, section): 62 60 def get_deck(self, section):
if not self.is_in_section(section): 63 61 if not self.is_in_section(section):
raise ObjectDoesNotExist("User not enrolled in section") 64 62 raise ObjectDoesNotExist("User not enrolled in section")
return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) 65 63 return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section)
66 64
67 65
class UserFlashcard(Model): 68 66 class UserFlashcard(Model):
""" 69 67 """
Represents the relationship between a user and a flashcard by: 70 68 Represents the relationship between a user and a flashcard by:
1. A user has a flashcard in their deck 71 69 1. A user has a flashcard in their deck
2. A user used to have a flashcard in their deck 72 70 2. A user used to have a flashcard in their deck
3. A user has a flashcard hidden from them 73 71 3. A user has a flashcard hidden from them
""" 74 72 """
user = ForeignKey('User') 75 73 user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None, 76 74 mask = MaskField(max_length=255, null=True, blank=True, default=None,
help_text="The user-specific mask on the card") 77 75 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") 78 76 pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card")
flashcard = ForeignKey('Flashcard') 79 77 flashcard = ForeignKey('Flashcard')
80 78
class Meta: 81 79 class Meta:
# There can be at most one UserFlashcard for each User and Flashcard 82 80 # There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),) 83 81 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 84 82 index_together = ["user", "flashcard"]
# By default, order by most recently pulled 85 83 # By default, order by most recently pulled
ordering = ['-pulled'] 86 84 ordering = ['-pulled']
87 85
88 86
class FlashcardHide(Model): 89 87 class FlashcardHide(Model):
""" 90 88 """
Represents the property of a flashcard being hidden by a user. 91 89 Represents the property of a flashcard being hidden by a user.
Each instance of this class represents a single user hiding a single flashcard. 92 90 Each instance of this class represents a single user hiding a single flashcard.
If reason is null, the flashcard was just hidden. 93 91 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. 94 92 If reason is not null, the flashcard was reported, and reason is the reason why it was reported.
""" 95 93 """
user = ForeignKey('User') 96 94 user = ForeignKey('User')
flashcard = ForeignKey('Flashcard') 97 95 flashcard = ForeignKey('Flashcard')
reason = CharField(max_length=255, blank=True, null=True) 98 96 reason = CharField(max_length=255, blank=True, null=True)
hidden = DateTimeField(auto_now_add=True) 99 97 hidden = DateTimeField(auto_now_add=True)
100 98
class Meta: 101 99 class Meta:
# There can only be one FlashcardHide object for each User and Flashcard 102 100 # There can only be one FlashcardHide object for each User and Flashcard
unique_together = (('user', 'flashcard'),) 103 101 unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"] 104 102 index_together = ["user", "flashcard"]
105 103
106 104
class Flashcard(Model): 107 105 class Flashcard(Model):
text = CharField(max_length=255, help_text='The text on the card') 108 106 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') 109 107 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") 110 108 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") 111 109 material_date = DateTimeField(default=now, help_text="The date with which the card is associated")
previous = ForeignKey('Flashcard', null=True, blank=True, 112 110 previous = ForeignKey('Flashcard', null=True, blank=True,
help_text="The previous version of this card, if one exists") 113 111 help_text="The previous version of this card, if one exists")
author = ForeignKey(User) 114 112 author = ForeignKey(User)
is_hidden = BooleanField(default=False) 115 113 is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card") 116 114 hide_reason = CharField(blank=True, max_length=255, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") 117 115 mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
118 116
class Meta: 119 117 class Meta:
# By default, order by most recently pushed 120 118 # By default, order by most recently pushed
ordering = ['-pushed'] 121 119 ordering = ['-pushed']
122 120
def is_hidden_from(self, user): 123 121 def is_hidden_from(self, user):
""" 124 122 """
A card can be hidden globally, but if a user has the card in their deck, 125 123 A card can be hidden globally, but if a user has the card in their deck,
this visibility overrides a global hide. 126 124 this visibility overrides a global hide.
:param user: 127 125 :param user:
:return: Whether the card is hidden from the user. 128 126 :return: Whether the card is hidden from the user.
""" 129 127 """
result = user.userflashcard_set.filter(flashcard=self) 130 128 if self.userflashcard_set.filter(user=user).exists(): return False
if not result.exists(): return self.is_hidden 131 129 if self.is_hidden or self.flashcardhide_set.filter(user=user).exists(): return True
return result[0].is_hidden() 132 130 return False
133 131
def edit(self, user, new_flashcard): 134 132 def edit(self, user, new_flashcard):
""" 135 133 """
Creates a new flashcard if a new flashcard should be created when the given user edits this flashcard. 136 134 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. 137 135 Sets up everything correctly so this object, when saved, will result in the appropriate changes.
:param user: The user editing this card. 138 136 :param user: The user editing this card.
:param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. 139 137 :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys.
""" 140 138 """
if not user.is_in_section(self.section): 141 139 if not user.is_in_section(self.section):
raise PermissionDenied("You don't have the permission to edit this card") 142 140 raise PermissionDenied("You don't have the permission to edit this card")
143 141
# content_changed is True iff either material_date or text were changed 144 142 # content_changed is True iff either material_date or text were changed
content_changed = False 145 143 content_changed = False
# create_new is True iff the user editing this card is the author of this card 146 144 # 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 147 145 # and there are no other users with this card in their decks
create_new = user != self.author or \ 148 146 create_new = user != self.author or \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() 149 147 UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
150 148
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: 151 149 if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']:
content_changed |= True 152 150 content_changed |= True
self.material_date = new_flashcard['material_date'] 153 151 self.material_date = new_flashcard['material_date']
if 'text' in new_flashcard and self.text != new_flashcard['text']: 154 152 if 'text' in new_flashcard and self.text != new_flashcard['text']:
content_changed |= True 155 153 content_changed |= True
self.text = new_flashcard['text'] 156 154 self.text = new_flashcard['text']
if create_new and content_changed: 157 155 if create_new and content_changed:
self.pk = None 158 156 self.pk = None
if 'mask' in new_flashcard: 159 157 if 'mask' in new_flashcard:
self.mask = new_flashcard['mask'] 160 158 self.mask = new_flashcard['mask']
self.save() 161 159 self.save()
162 160
@classmethod 163 161 @classmethod
def cards_visible_to(cls, user): 164 162 def cards_visible_to(cls, user):
""" 165 163 """
:param user: 166 164 :param user:
:return: A queryset with all cards that should be visible to a user. 167 165 :return: A queryset with all cards that should be visible to a user.
""" 168 166 """
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) 169 167 return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
170 168
171 169
class UserFlashcardQuiz(Model): 172 170 class UserFlashcardQuiz(Model):
""" 173 171 """
An event of a user being quizzed on a flashcard. 174 172 An event of a user being quizzed on a flashcard.
""" 175 173 """
user_flashcard = ForeignKey(UserFlashcard) 176 174 user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True) 177 175 when = DateTimeField(auto_now=True)
blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked") 178 176 blanked_word = CharField(max_length=8, blank=True, help_text="The character range which was blanked")
response = CharField(max_length=255, blank=True, null=True, help_text="The user's response") 179 177 response = CharField(max_length=255, blank=True, null=True, help_text="The user's response")
correct = NullBooleanField(help_text="The user's self-evaluation of their response") 180 178 correct = NullBooleanField(help_text="The user's self-evaluation of their response")
181 179
def status(self): 182 180 def status(self):
""" 183 181 """
There are three stages of a quiz object: 184 182 There are three stages of a quiz object:
1. the user has been shown the card 185 183 1. the user has been shown the card
2. the user has answered the card 186 184 2. the user has answered the card
3. the user has self-evaluated their response's correctness 187 185 3. the user has self-evaluated their response's correctness
188 186
:return: string (evaluated, answered, viewed) 189 187 :return: string (evaluated, answered, viewed)
""" 190 188 """
if self.correct is not None: return "evaluated" 191 189 if self.correct is not None: return "evaluated"
if self.response: return "answered" 192 190 if self.response: return "answered"
return "viewed" 193 191 return "viewed"
194 192
195 193
class Section(Model): 196 194 class Section(Model):
""" 197 195 """
A UCSD course taught by an instructor during a quarter. 198 196 A UCSD course taught by an instructor during a quarter.
We use the term "section" to avoid collision with the builtin keyword "class" 199 197 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 200 198 We index gratuitously to support autofill and because this is primarily read-only
""" 201 199 """
department = CharField(db_index=True, max_length=50) 202 200 department = CharField(db_index=True, max_length=50)
department_abbreviation = CharField(db_index=True, max_length=10) 203 201 department_abbreviation = CharField(db_index=True, max_length=10)
course_num = CharField(db_index=True, max_length=6) 204 202 course_num = CharField(db_index=True, max_length=6)
course_title = CharField(db_index=True, max_length=50) 205 203 course_title = CharField(db_index=True, max_length=50)
instructor = CharField(db_index=True, max_length=100) 206 204 instructor = CharField(db_index=True, max_length=100)
quarter = CharField(db_index=True, max_length=4) 207 205 quarter = CharField(db_index=True, max_length=4)
208 206
@classmethod 209 207 @classmethod
def search(cls, terms): 210 208 def search(cls, terms):
""" 211 209 """
Search all fields of all sections for a particular set of terms 212 210 Search all fields of all sections for a particular set of terms
A matching section must match at least one field on each term 213 211 A matching section must match at least one field on each term
:param terms:iterable 214 212 :param terms:iterable
:return: Matching QuerySet ordered by department and course number 215 213 :return: Matching QuerySet ordered by department and course number
""" 216 214 """
final_q = Q() 217 215 final_q = Q()
for term in terms: 218 216 for term in terms:
q = Q(department__icontains=term) 219 217 q = Q(department__icontains=term)
q |= Q(department_abbreviation__icontains=term) 220 218 q |= Q(department_abbreviation__icontains=term)
q |= Q(course_title__icontains=term) 221 219 q |= Q(course_title__icontains=term)
q |= Q(course_num__icontains=term) 222 220 q |= Q(course_num__icontains=term)
q |= Q(instructor__icontains=term) 223 221 q |= Q(instructor__icontains=term)
final_q &= q 224 222 final_q &= q
qs = cls.objects.filter(final_q) 225 223 qs = cls.objects.filter(final_q)
# Have the database cast the course number to an integer so it will sort properly 226 224 # 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 227 225 # 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)"}) 228 226 qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int') 229 227 qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs 230 228 return qs
231 229
@property 232 230 @property
def is_whitelisted(self): 233 231 def is_whitelisted(self):
""" 234 232 """
:return: whether a whitelist exists for this section 235 233 :return: whether a whitelist exists for this section
""" 236 234 """
return self.whitelist.exists() 237 235 return self.whitelist.exists()
238 236
def is_user_on_whitelist(self, user): 239 237 def is_user_on_whitelist(self, user):
""" 240 238 """
:return: whether the user is on the waitlist for this section 241 239 :return: whether the user is on the waitlist for this section
""" 242 240 """
return self.whitelist.filter(email=user.email).exists() 243 241 return self.whitelist.filter(email=user.email).exists()
244 242
class Meta: 245 243 class Meta:
ordering = ['-course_title'] 246 244 ordering = ['-course_title']
247 245
@property 248 246 @property
def lecture_times(self): 249 247 def lecture_times(self):
lecture_periods = self.lectureperiod_set.all() 250 248 lecture_periods = self.lectureperiod_set.all()
if not lecture_periods.exists(): return '' 251 249 if not lecture_periods.exists(): return ''
return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time 252 250 return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time
253 251