models.py 12.4 KB
from datetime import datetime
from django.contrib.auth.models import AbstractUser, UserManager
from django.core.exceptions import PermissionDenied, SuspiciousOperation
from django.db import IntegrityError
from django.db.models import *
from django.utils.timezone import now
from simple_email_confirmation import SimpleEmailConfirmationUserMixin
from fields import MaskField
# Hack to fix AbstractUser before subclassing it
AbstractUser._meta.get_field('email')._unique = True
AbstractUser._meta.get_field('username')._unique = False
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):
"""
Creates and saves a User with the given email and password.
"""
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)
def create_superuser(self, email, password, **extra_fields):
return self._create_user(email, password, True, True, **extra_fields)
class User(AbstractUser, SimpleEmailConfirmationUserMixin):
"""
An extension of Django's default user model.
We use email as the username field, and include enrolled sections here
"""
objects = EmailOnlyUserManager()
USERNAME_FIELD = 'email'
REQUIRED_FIELDS = []
sections = ManyToManyField('Section', help_text="The sections which the user is enrolled in")
def is_in_section(self, section):
return self.sections.filter(pk=section.pk).exists()
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()
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)
class UserFlashcard(Model):
"""
Represents the relationship between a user and a flashcard by:
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
"""
user = ForeignKey('User')
mask = MaskField(max_length=255, null=True, blank=True, default=None,
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")
flashcard = ForeignKey('Flashcard')
class Meta:
# There can be at most one UserFlashcard for each User and Flashcard
unique_together = (('user', 'flashcard'),)
index_together = ["user", "flashcard"]
# By default, order by most recently pulled
ordering = ['-pulled']
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"]
class Flashcard(Model):
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')
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")
previous = ForeignKey('Flashcard', null=True, blank=True, default=None,
help_text="The previous version of this card, if one exists")
author = ForeignKey(User)
is_hidden = BooleanField(default=False)
hide_reason = CharField(blank=True, null=True, max_length=255,
default=None, help_text="Reason for hiding this card")
mask = MaskField(max_length=255, null=True, blank=True, help_text="The mask on the card")
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()
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
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 \
UserFlashcard.objects.filter(flashcard=self).exclude(user=user).exists()
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:
mask = self.mask
pk = self.pk
self.pk = None
if 'mask' in new_flashcard:
mask = new_flashcard['mask']
self.mask = mask
self.previous_id = pk
self.save()
return create_new and content_changed
@classmethod
def cards_visible_to(cls, user):
"""
:param user:
:return: A queryset with all cards that should be visible to a user.
"""
return cls.objects.filter(is_hidden=False).exclude(flashcardhide__user=user)
class UserFlashcardQuiz(Model):
"""
An event of a user being quizzed on a flashcard.
"""
user_flashcard = ForeignKey(UserFlashcard)
when = DateTimeField(auto_now=True)
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):
"""
There are three stages of a quiz object:
1. the user has been shown the card
2. the user has answered the card
3. the user has self-evaluated their response's correctness
:return: string (evaluated, answered, viewed)
"""
if self.correct is not None: return "evaluated"
if self.response: return "answered"
return "viewed"
class Section(Model):
"""
A UCSD course taught by an instructor during a quarter.
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
"""
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)
@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
qs = qs.extra(select={'course_num_int': "CAST(rtrim(course_num, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ') AS INTEGER)"})
qs = qs.order_by('department_abbreviation', 'course_num_int')
return qs
@property
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
"""
return self.whitelist.filter(email=user.email).exists()
class Meta:
ordering = ['-course_title']
@property
def lecture_times(self):
lecture_periods = self.lectureperiod_set.all()
if not lecture_periods.exists(): return ''
return ''.join(map(lambda x: x.weekday_letter, lecture_periods)) + ' ' + lecture_periods[0].short_start_time
@property
def long_name(self):
return '%s %s (%s)' % (self.course_title, self.lecture_times, self.instructor)
@property
def short_name(self):
return '%s %s' % (self.department_abbreviation, self.course_num)
def get_feed_for_user(self, user):
qs = Flashcard.objects.filter(section=self).exclude(userflashcard__user=user).order_by('pushed')
return qs
def __unicode__(self):
return '%s %s: %s (%s %s)' % (
self.department_abbreviation, self.course_num, self.course_title, self.instructor, self.quarter)
class LecturePeriod(Model):
"""
A lecture period for a section
"""
section = ForeignKey(Section)
week_day = IntegerField(help_text="1-indexed day of week, starting at Sunday")
start_time = TimeField()
end_time = TimeField()
@property
def weekday_letter(self):
return ['?', 'S', 'M', 'T', 'W', 'Th', 'F', 'S', 'S'][self.week_day]
@property
def short_start_time(self):
# lstrip 0 because windows doesn't support %-I
return self.start_time.strftime('%I %p').lstrip('0')
class Meta:
unique_together = (('section', 'start_time', 'week_day'),)
ordering = ['section', 'week_day']
class WhitelistedAddress(Model):
"""
An email address that has been whitelisted for a section at an instructor's request
"""
email = EmailField()
section = ForeignKey(Section, related_name='whitelist')