Blame view
flashcards/models.py
12.4 KB
a2d8c4229
|
1 |
from datetime import datetime |
9276b7b89
|
2 |
from django.contrib.auth.models import AbstractUser, UserManager |
5d861cbfb
|
3 4 |
from django.core.exceptions import PermissionDenied, SuspiciousOperation from django.db import IntegrityError |
d6a663553
|
5 |
from django.db.models import * |
ddd9b2145
|
6 |
from django.utils.timezone import now |
18095ed46
|
7 |
from simple_email_confirmation import SimpleEmailConfirmationUserMixin |
fe6a4ff63
|
8 |
from fields import MaskField |
a2d8c4229
|
9 |
|
18095ed46
|
10 |
|
2b6bc762e
|
11 12 |
# Hack to fix AbstractUser before subclassing it AbstractUser._meta.get_field('email')._unique = True |
ce17f969f
|
13 |
AbstractUser._meta.get_field('username')._unique = False |
18095ed46
|
14 |
|
9f4aa9bfa
|
15 |
|
ddd9b2145
|
16 17 18 19 20 21 22 23 |
class EmailOnlyUserManager(UserManager): """ A tiny extension of Django's UserManager which correctly creates users without usernames (using emails instead). """ def _create_user(self, email, password, is_staff, is_superuser, **extra_fields): """ |
bf4ae95e4
|
24 |
Creates and saves a User with the given email and password. |
ddd9b2145
|
25 26 27 28 29 30 31 32 33 34 35 36 |
""" email = self.normalize_email(email) user = self.model(email=email, is_staff=is_staff, is_active=True, is_superuser=is_superuser, date_joined=now(), **extra_fields) user.set_password(password) user.save(using=self._db) return user def create_user(self, email, password=None, **extra_fields): return self._create_user(email, password, False, False, **extra_fields) |
56c04ca5b
|
37 |
def create_superuser(self, email, password, **extra_fields): |
ddd9b2145
|
38 |
return self._create_user(email, password, True, True, **extra_fields) |
56c04ca5b
|
39 40 41 |
class User(AbstractUser, SimpleEmailConfirmationUserMixin): |
ddd9b2145
|
42 43 44 45 46 |
""" An extension of Django's default user model. We use email as the username field, and include enrolled sections here """ objects = EmailOnlyUserManager() |
18095ed46
|
47 48 |
USERNAME_FIELD = 'email' REQUIRED_FIELDS = [] |
ddd9b2145
|
49 |
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in") |
e0cc79368
|
50 |
|
f0b284adb
|
51 |
def is_in_section(self, section): |
fedcc8ded
|
52 |
return self.sections.filter(pk=section.pk).exists() |
f0b284adb
|
53 54 55 56 57 58 59 |
def pull(self, flashcard): if not self.is_in_section(flashcard.section): raise ValueError("User not in the section this flashcard belongs to") user_card = UserFlashcard.objects.create(user=self, flashcard=flashcard) user_card.pulled = datetime.now() user_card.save() |
fedcc8ded
|
60 61 62 63 |
def get_deck(self, section): if not self.is_in_section(section): raise ObjectDoesNotExist("User not enrolled in section") return Flashcard.objects.all().filter(userflashcard__user=self).filter(section=section) |
8b6e41f08
|
64 |
|
d6a663553
|
65 |
class UserFlashcard(Model): |
e0cc79368
|
66 67 |
""" Represents the relationship between a user and a flashcard by: |
8b6e41f08
|
68 69 70 |
1. A user has a flashcard in their deck 2. A user used to have a flashcard in their deck 3. A user has a flashcard hidden from them |
e0cc79368
|
71 |
""" |
2b6bc762e
|
72 |
user = ForeignKey('User') |
fe0d37016
|
73 74 |
mask = MaskField(max_length=255, null=True, blank=True, default=None, help_text="The user-specific mask on the card") |
bca16d61f
|
75 |
pulled = DateTimeField(blank=True, null=True, default=None, help_text="When the user pulled the card") |
d6a663553
|
76 |
flashcard = ForeignKey('Flashcard') |
8b6e41f08
|
77 78 |
class Meta: |
d6a663553
|
79 |
# There can be at most one UserFlashcard for each User and Flashcard |
8b6e41f08
|
80 81 |
unique_together = (('user', 'flashcard'),) index_together = ["user", "flashcard"] |
d6a663553
|
82 |
# By default, order by most recently pulled |
8b6e41f08
|
83 |
ordering = ['-pulled'] |
8b6e41f08
|
84 |
|
f0b284adb
|
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 |
class FlashcardHide(Model): """ Represents the property of a flashcard being hidden by a user. Each instance of this class represents a single user hiding a single flashcard. 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. """ user = ForeignKey('User') flashcard = ForeignKey('Flashcard') reason = CharField(max_length=255, blank=True, null=True) hidden = DateTimeField(auto_now_add=True) class Meta: # There can only be one FlashcardHide object for each User and Flashcard unique_together = (('user', 'flashcard'),) index_together = ["user", "flashcard"] |
8b6e41f08
|
101 |
|
c663d9897
|
102 |
|
d6a663553
|
103 104 |
class Flashcard(Model): text = CharField(max_length=255, help_text='The text on the card') |
491577131
|
105 |
section = ForeignKey('Section', help_text='The section with which the card is associated') |
d6a663553
|
106 |
pushed = DateTimeField(auto_now_add=True, help_text="When the card was first pushed") |
c1f4d3dea
|
107 |
material_date = DateTimeField(default=now, help_text="The date with which the card is associated") |
5d861cbfb
|
108 |
previous = ForeignKey('Flashcard', null=True, blank=True, default=None, |
d6a663553
|
109 110 111 |
help_text="The previous version of this card, if one exists") author = ForeignKey(User) is_hidden = BooleanField(default=False) |
5d861cbfb
|
112 113 |
hide_reason = CharField(blank=True, null=True, max_length=255, default=None, help_text="Reason for hiding this card") |
fe6a4ff63
|
114 |
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card") |
8b6e41f08
|
115 |
|
d6a663553
|
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
class Meta: # By default, order by most recently pushed ordering = ['-pushed'] def is_hidden_from(self, user): """ A card can be hidden globally, but if a user has the card in their deck, this visibility overrides a global hide. :param user: :return: Whether the card is hidden from the user. """ result = user.userflashcard_set.filter(flashcard=self) if not result.exists(): return self.is_hidden return result[0].is_hidden() |
8b6e41f08
|
130 |
|
5d861cbfb
|
131 132 133 134 135 136 137 138 139 |
def add_to_deck(self, user): if not user.is_in_section(self.section): raise PermissionDenied("You don't have the permission to add this card") try: user_flashcard = UserFlashcard.objects.create(user=user, flashcard=self, mask=self.mask) except IntegrityError: raise SuspiciousOperation("The flashcard is already in the user's deck") user_flashcard.save() return user_flashcard |
f0b284adb
|
140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
def edit(self, user, new_flashcard): """ 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. :param user: The user editing this card. :param new_flashcard: The new information, namely a dict containg 'material_date', 'text', and 'mask' keys. """ if not user.is_in_section(self.section): raise PermissionDenied("You don't have the permission to edit this card") # content_changed is True iff either material_date or text were changed content_changed = False # 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 create_new = user != self.author or \ |
a2d8c4229
|
155 |
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists() |
f0b284adb
|
156 157 158 159 160 161 162 163 |
if 'material_date' in new_flashcard and self.material_date != new_flashcard['material_date']: content_changed |= True self.material_date = new_flashcard['material_date'] if 'text' in new_flashcard and self.text != new_flashcard['text']: content_changed |= True self.text = new_flashcard['text'] if create_new and content_changed: |
5d861cbfb
|
164 165 |
mask = self.mask pk = self.pk |
f0b284adb
|
166 167 |
self.pk = None if 'mask' in new_flashcard: |
5d861cbfb
|
168 169 170 |
mask = new_flashcard['mask'] self.mask = mask self.previous_id = pk |
fedcc8ded
|
171 |
self.save() |
5d861cbfb
|
172 |
return create_new and content_changed |
f0b284adb
|
173 |
|
8b6e41f08
|
174 175 |
@classmethod def cards_visible_to(cls, user): |
d6a663553
|
176 177 178 179 |
""" :param user: :return: A queryset with all cards that should be visible to a user. """ |
033fffbf8
|
180 |
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user) |
d6a663553
|
181 |
|
8b6e41f08
|
182 |
|
1519ec887
|
183 |
class UserFlashcardQuiz(Model): |
d6a663553
|
184 |
""" |
1519ec887
|
185 |
An event of a user being quizzed on a flashcard. |
d6a663553
|
186 187 |
""" user_flashcard = ForeignKey(UserFlashcard) |
c1f4d3dea
|
188 |
when = DateTimeField(auto_now=True) |
d6a663553
|
189 190 191 192 193 194 |
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") correct = NullBooleanField(help_text="The user's self-evaluation of their response") def status(self): """ |
1519ec887
|
195 |
There are three stages of a quiz object: |
d6a663553
|
196 197 |
1. the user has been shown the card 2. the user has answered the card |
08fbe6868
|
198 |
3. the user has self-evaluated their response's correctness |
55c1546aa
|
199 |
|
d6a663553
|
200 201 |
:return: string (evaluated, answered, viewed) """ |
08fbe6868
|
202 |
if self.correct is not None: return "evaluated" |
d6a663553
|
203 204 |
if self.response: return "answered" return "viewed" |
ab5a72acd
|
205 |
|
18095ed46
|
206 |
|
491577131
|
207 |
class Section(Model): |
d6a663553
|
208 209 |
""" A UCSD course taught by an instructor during a quarter. |
491577131
|
210 |
We use the term "section" to avoid collision with the builtin keyword "class" |
dc685f192
|
211 |
We index gratuitously to support autofill and because this is primarily read-only |
d6a663553
|
212 |
""" |
2c22131d9
|
213 214 215 216 217 218 |
department = CharField(db_index=True, max_length=50) department_abbreviation = CharField(db_index=True, max_length=10) course_num = CharField(db_index=True, max_length=6) course_title = CharField(db_index=True, max_length=50) instructor = CharField(db_index=True, max_length=100) quarter = CharField(db_index=True, max_length=4) |
72bf5f00c
|
219 |
|
dc685f192
|
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 |
@classmethod def search(cls, terms): """ Search all fields of all sections for a particular set of terms A matching section must match at least one field on each term :param terms:iterable :return: Matching QuerySet ordered by department and course number """ final_q = Q() for term in terms: q = Q(department__icontains=term) q |= Q(department_abbreviation__icontains=term) q |= Q(course_title__icontains=term) q |= Q(course_num__icontains=term) q |= Q(instructor__icontains=term) final_q &= q qs = cls.objects.filter(final_q) # 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 |
52c37d174
|
239 |
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"}) |
dc685f192
|
240 241 |
qs = qs.order_by('department_abbreviation', 'course_num_int') return qs |
7b4e8b793
|
242 |
@property |
72bf5f00c
|
243 244 245 246 247 248 249 250 251 252 |
def is_whitelisted(self): """ :return: whether a whitelist exists for this section """ return self.whitelist.exists() def is_user_on_whitelist(self, user): """ :return: whether the user is on the waitlist for this section """ |
72bf5f00c
|
253 |
return self.whitelist.filter(email=user.email).exists() |
8b6e41f08
|
254 |
|
d6a663553
|
255 |
class Meta: |
be7810aad
|
256 |
ordering = ['-course_title'] |
491577131
|
257 |
|
0794ea949
|
258 |
@property |
7b4e8b793
|
259 |
def lecture_times(self): |
fe0d37016
|
260 |
lecture_periods = self.lectureperiod_set.all() |
02dee7072
|
261 |
if not lecture_periods.exists(): return '' |
fe0d37016
|
262 |
return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time |
7b4e8b793
|
263 264 |
@property |
0794ea949
|
265 |
def long_name(self): |
a2d8c4229
|
266 |
return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor) |
0794ea949
|
267 268 269 270 |
@property def short_name(self): return '%s %s' % (self.department_abbreviation, self.course_num) |
a2d8c4229
|
271 272 273 |
def get_feed_for_user(self, user): qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed') return qs |
2c22131d9
|
274 275 |
def __unicode__(self): return '%s %s: %s (%s %s)' % ( |
0794ea949
|
276 |
self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter) |
2c22131d9
|
277 |
|
fe0d37016
|
278 |
|
491577131
|
279 280 281 282 283 |
class LecturePeriod(Model): """ A lecture period for a section """ section = ForeignKey(Section) |
ddd9b2145
|
284 |
week_day = IntegerField(help_text="1-indexed day of week, starting at Sunday") |
491577131
|
285 286 |
start_time = TimeField() end_time = TimeField() |
9f4aa9bfa
|
287 |
|
7b4e8b793
|
288 289 290 291 292 293 |
@property def weekday_letter(self): return ['?', 'S', 'M', 'T', 'W', 'Th', 'F', 'S', 'S'][self.week_day] @property def short_start_time(self): |
dc19eeed5
|
294 295 |
# lstrip 0 because windows doesn't support %-I return self.start_time.strftime('%I %p').lstrip('0') |
7b4e8b793
|
296 |
|
9f4aa9bfa
|
297 298 |
class Meta: unique_together = (('section', 'start_time', 'week_day'),) |
7b4e8b793
|
299 |
ordering = ['section', 'week_day'] |
9f4aa9bfa
|
300 301 302 |
class WhitelistedAddress(Model): |
ddd9b2145
|
303 304 305 |
""" An email address that has been whitelisted for a section at an instructor's request """ |
9f4aa9bfa
|
306 |
email = EmailField() |
72bf5f00c
|
307 |
section = ForeignKey(Section, related_name='whitelist') |