Source code for timetable.models

# Copyright (C) 2017 Semester.ly Technologies, LLC
#
# Semester.ly is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Semester.ly is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

import os

os.environ['DJANGO_SETTINGS_MODULE'] = 'semesterly.settings'

from django.forms.models import model_to_dict
from django.db import models
from picklefield.fields import PickledObjectField


[docs]class Semester(models.Model): """ Represents a semester which is composed of a name (e.g. Spring, Fall) and a year (e.g. 2017). Attributes: name (CharField): the name (e.g. Spring, Fall) year (CharField): the year (e.g. 2017, 2018) """ name = models.CharField(max_length=50) year = models.CharField(max_length=4) def __unicode__(self): return '{} {}'.format(self.name, self.year) def __str__(self): return '{} {}'.format(self.name, self.year)
[docs]class Textbook(models.Model): """ A textbook which is associated with sections of courses. Stores information from the Amazon product API including a detail url and ISBN. Attributes: isbn (BigIntegerField): the primary (unique) key ISBN number detail_url (URLField): url to the detail page on Amazon.com image_url (URLField): url to product image hosted on Amazon.com author (CharField): authors first and last name title (CharField): the title of the book """ isbn = models.BigIntegerField(primary_key=True) detail_url = models.URLField(max_length=1000, null=True) image_url = models.URLField(max_length=1000, null=True) author = models.CharField(max_length=500, null=True) title = models.CharField(max_length=1500, null=True) def get_info(self): return model_to_dict(self)
[docs]class Course(models.Model): """ Represents a course at a school, made unique by its course code. Courses persist across semesters and years. Their presence in a semester or year is indicated by the existence of sections assigned to that course for that semester or year. This is why a course does not have fields like professor, those varies. The course model maintains only attributes which tend not to vary across semesters or years. A course has many :obj:`Section` which a student can enroll in. Attributes: school (:obj:`CharField`): the school code corresponding to the school for the course code (:obj:`CharField`): the course code without indication of section (E.g. EN.600.100) name (:obj:`CharField`): the general name of the course (E.g. Calculus I) description (:obj:`TextField`): the explanation of the content of the courzse notes (:obj:`TextField`, optional): usually notes pertaining to registration (e.g. Lab Fees) info (:obj:`TextField`, optional): similar to notes unstopped_description (:obj:`TextField`): automatically generated description without stopwords campus (:obj:`CharField`, optional): an indicator for which campus the course is taught on prerequisites (:obj:`TextField`, optional): courses required before taking this course corequisites (:obj:`TextField`, optional): courses required concurrently with this course exclusions (:obj:`TextField`, optional): reasons why a student would not be able to take this num_credits (:obj:`FloatField`): the number of credit hours this course is worth areas (:obj:`CharField`): comma seperated list of all degree areas this course satisfies department (:obj:`CharField`): department offering course (e.g. Computer Science) level (:obj:`CharField`): indicator of level of course (e.g. 100, 200, Upper, Lower, Grad) cores (:obj:`CharField`): core areas satisfied by this course geneds (:obj:`CharField`): geneds satisfied by this course related_courses (:obj:`ManyToManyField` of :obj:`Course`, optional): courses computed similar to this course same_as (:obj:`ForeignKey`): If this course is the same as another course, provide Foreign key vector (:obj:`PickleObjectField`): the vector representation of a course transformed from course vectorizer """ school = models.CharField(db_index=True, max_length=100) code = models.CharField(max_length=20) name = models.CharField(max_length=250) description = models.TextField(default='') notes = models.TextField(default='', null=True) info = models.TextField(default='', null=True) unstopped_description = models.TextField(default='') campus = models.CharField(max_length=300, default='') prerequisites = models.TextField(default='', null=True) corequisites = models.TextField(default='', null=True) exclusions = models.TextField(default='') num_credits = models.FloatField(default=-1) areas = models.CharField(max_length=600, default='', null=True) department = models.CharField(max_length=250, default='', null=True) level = models.CharField(max_length=500, default='', null=True) # TODO generalize core/gened/breadth field cores = models.CharField(max_length=50, null=True, blank=True) geneds = models.CharField(max_length=300, null=True, blank=True) related_courses = models.ManyToManyField('self', blank=True) same_as = models.ForeignKey('self', null=True) vector = PickledObjectField(default=None, null=True) def __str__(self): return self.code + ": " + self.name
[docs] def get_reactions(self, student=None): """ Return a list of dicts for each type of reaction (by title) for this course. Each dict has: **title**: the title of the reaction **count:** number of reactions with this title that this course has received **reacted:** True if the student provided has given a reaction with this title """ result = list(self.reaction_set.values('title') \ .annotate(count=models.Count('title')).distinct().all()) if not student: return result # TODO: rewrite as a single DB query for i, reaction in enumerate(result): result[i]['reacted'] = self.reaction_set.filter(student=student, title=reaction['title']).exists() return result
[docs] def get_avg_rating(self): """ Calculates the avg rating for a course, -1 if no ratings. Includes all courses that are marked as the same by the self.same_as field on the model nstance. Returns: (:obj:`float`): the average course rating """ ratings_sum, ratings_count = self._get_ratings_sum_count() if self.same_as: # include ratings for equivalent courses in the average eq_sum, eq_count = self.same_as._get_ratings_sum_count() ratings_sum += eq_sum ratings_count += eq_count return (ratings_sum / ratings_count) if ratings_count else -1
def _get_ratings_sum_count(self): """ Return the sum and count of ratings of this course not counting equivalent courses. """ ratings = Evaluation.objects.only('course', 'score').filter(course=self) return sum([rating.score for rating in ratings]), len(ratings)
[docs]class Section(models.Model): """ Represents one (of possibly many) choice(s) for a student to enroll in a :obj:`Course` for a specific semester. Since this model is specific to a semester, it contains enrollment data, instructor information, textbooks, etc. A section can come in different forms. For example, a lecture which is required for every student. However, it can also be a tutorial or practical. During timetable generation we allow a user to select one of each, and we can automatically choose the best combonation for a user as well. A section has many offerings related to it. For example, section 1 of a :obj:`Course` could have 3 offerings (one that meets each day: Monday, Wednesday, Friday). Section 2 of a :obj:`Course` could have 3 other offerings (one that meets each: Tuesday, Thursday). Attributes: course (:obj:`Course`): The course this section belongs to meeting_section (:obj:`CharField`): the name of the section (e.g. 001, L01, LAB2) size (:obj:`IntegerField`): the capacity of the course (the enrollment cap) enrolment (:obj:`IntegerField`): the number of students registered so far waitlist (:obj:`IntegerField`): the number of students waitlisted so far waitlist_size (:obj:`IntegerField`): the max size of the waitlist section_type (:obj:`CharField`): the section type, example 'L' is lecture, 'T' is tutorial, `P` is practical instructors (:obj:`CharField`): comma seperated list of instructors semester (:obj:`ForeignKey` to :obj:`Semester`): the semester for the section textbooks (:obj:`ManyToManyField` of :obj:`Textbook`): textbooks for this section via the :obj:`TextbookLink` model was_full (:obj:`BooleanField`): whether the course was full during the last parse """ course = models.ForeignKey(Course) meeting_section = models.CharField(max_length=50) size = models.IntegerField(default=-1) enrolment = models.IntegerField(default=-1) waitlist = models.IntegerField(default=-1) waitlist_size = models.IntegerField(default=-1) section_type = models.CharField(max_length=50, default='L') instructors = models.CharField(max_length=500, default='TBA') semester = models.ForeignKey(Semester) textbooks = models.ManyToManyField(Textbook, through='TextbookLink') was_full = models.BooleanField(default=False)
[docs] def get_textbooks(self): """ Returns the textbook info using `tb.get_info()` for each textbook """ return [tb.get_info() for tb in self.textbooks.all()]
def is_full(self): return self.enrolment >= 0 and self.size >= 0 and self.enrolment >= self.size def __str__(self): return "Course: {0}; Section: {0}; Semester: {0}".format(self.course, self.meeting_section, self.semester)
[docs]class Offering(models.Model): """ An Offering is the most granular part of the Course heirarchy. An offering may be looked at as the backend equivalent of a single slot on a timetable. For each day/time which a section meets, an offering is created.abs Attributes: section (:obj:`ForeignKey` to :obj:`Section`): the section which is the parent of this offering day (:obj:`CharField`): the day the course is offered (single character M,T,W,R,F,S,U) time_start (:obj:`CharField`): the time the slot starts in 24hrs time in the format (HH:MM) or (H:MM) time_end (:obj:`CharField`): the time it ends in 24hrs time in the format (HH:MM) or (H:MM) location (:obj:`CharField`, optional): the location the course takes place, defaulting to TBA if not provided """ section = models.ForeignKey(Section) day = models.CharField(max_length=1) time_start = models.CharField(max_length=15) time_end = models.CharField(max_length=15) location = models.CharField(max_length=200, default='TBA') def __unicode__(self): return "Day: %s, Time: %s - %s" % (self.day, self.time_start, self.time_end)
[docs]class Evaluation(models.Model): """ A review of a course represented as a score out of 5, a summary/comment, along with the professor and year the review is in subject of. course (:obj:`ForeignKey` to :obj:`Course`): the course this evaluation belongs to score (:obj:`FloatField`): score out of 5.0 summary (:obj:`TextField`): text with information about why the rating was given professor (:obj:`CharField`): the professor(s) this review pertains to year (:obj:`CharField`): the year of the review course_code (:obj:`Charfield`): a string of the course code, along with section indicator """ course = models.ForeignKey(Course) score = models.FloatField(default=5.0) summary = models.TextField() professor = models.CharField(max_length=250) course_code = models.CharField(max_length=20) year = models.CharField(max_length=200)
[docs]class Integration(models.Model): name = models.CharField(max_length=250)
[docs]class CourseIntegration(models.Model): course = models.ForeignKey(Course) integration = models.ForeignKey(Integration) json = models.TextField()
class Timetable(models.Model): courses = models.ManyToManyField(Course) sections = models.ManyToManyField(Section) semester = models.ForeignKey(Semester) school = models.CharField(max_length=50) class Meta: abstract = True