Commit dc19eeed5e569bbdcf7a1a9aec18179ca60620bc

Authored by Andrew Buss
1 parent 29c4330965
Exists in master

replaced strftime with something more cross platform(?)

Showing 1 changed file with 2 additions and 1 deletions Inline Diff

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