Source code for timetable.models

# -*- coding: utf-8 -*-
# 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
from django.contrib.postgres.fields import ArrayField


[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 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`): this course's school's code 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 course 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:`Arrayfield`): 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 """ school = models.CharField(db_index=True, max_length=100) code = models.CharField(max_length=20) name = models.CharField(max_length=255) 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) department = models.CharField(max_length=255, 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, on_delete=models.deletion.CASCADE) pos = ArrayField(models.TextField(default="", null=True), default=list) areas = ArrayField(models.TextField(default="", null=True), default=list) sub_school = models.TextField(default="", null=True) writing_intensive = models.TextField(default="", 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 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 instance. 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) def __unicode__(self): return "%s" % (self.name)
[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, 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 combination 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 was_full (:obj:`BooleanField`): whether the course was full during the last parse course_section_id (:obj:`IntegerField`): the id of the section when sending data to SIS """ course = models.ForeignKey(Course, on_delete=models.deletion.CASCADE) 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, on_delete=models.deletion.CASCADE) was_full = models.BooleanField(default=False) course_section_id = models.IntegerField(default=0) 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 ) def __unicode__(self): return "Course: %s; Section: %s; Semester: %s" % ( 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, on_delete=models.deletion.CASCADE) day = models.CharField(max_length=1) date_start = models.CharField(max_length=15, null=True) date_end = models.CharField(max_length=15, null=True) time_start = models.CharField(max_length=15) time_end = models.CharField(max_length=15) location = models.CharField(max_length=200, default="TBA") is_short_course = models.BooleanField(default=False) 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. Attributes: 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, on_delete=models.deletion.CASCADE) score = models.FloatField(default=5.0) summary = models.TextField() professor = models.CharField(max_length=255) course_code = models.CharField(max_length=20) year = models.CharField(max_length=200)
[docs]class Integration(models.Model): name = models.CharField(max_length=255)
[docs]class CourseIntegration(models.Model): course = models.ForeignKey(Course, on_delete=models.deletion.CASCADE) integration = models.ForeignKey(Integration, on_delete=models.deletion.CASCADE) json = models.TextField() semester = models.ManyToManyField(Semester)
class Timetable(models.Model): courses = models.ManyToManyField(Course) sections = models.ManyToManyField(Section) semester = models.ForeignKey(Semester, on_delete=models.deletion.CASCADE) school = models.CharField(max_length=50) class Meta: abstract = True