from django.db import models
from django.contrib.auth.models import User
from django.core.validators import (
MinValueValidator,
MaxValueValidator,
MinLengthValidator,
MaxLengthValidator,
)
from django.db.models import Avg
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator
[docs]
class Course(models.Model):
"""
Course model representing an academic course at Amherst College.
Parameters
----------
id : IntegerField, primary key
Unique 7-digit course identifier
Format: XYYZCZZ where:
X: College/semester (4=Amherst Fall, 5=Amherst Spring)
YY: Department code (00-99)
Z: Credit flag (1=half-credit, 0=full-credit)
CZZ: Course number (000-999)
Example: 4140112 = Fall COSC-112
courseLink : URLField, optional
Direct URL to the course page on Amherst College's course catalog
Defaults to null if not provided
courseName : CharField
Full title of the course as it appears in the course catalog
Maximum length: 200 characters
courseCodes : ManyToManyField to CourseCode
Official course codes/numbers (e.g., "COSC-111", "MATH-271")
Multiple codes possible for cross-listed courses
courseDescription : TextField, optional
Detailed description of course content, objectives, and requirements
Can be blank
courseMaterialsLink : URLField, optional
URL to course materials, syllabus, or additional resources
Defaults to null if not provided
keywords : ManyToManyField to Keyword
Searchable keywords/tags associated with the course content
divisions : ManyToManyField to Division
Academic divisions the course belongs to (e.g., "Science", "Humanities")
departments : ManyToManyField to Department
Academic departments offering the course
enrollmentText : TextField
Instructions for course enrollment and registration
Defaults to "Contact the professor for enrollment details"
prefForMajor : BooleanField
Whether majors have enrollment priority
Defaults to False
overallCap : PositiveIntegerField
Maximum total enrollment limit
Defaults to 0 (no cap)
freshmanCap : PositiveIntegerField
Maximum number of freshmen allowed to enroll
Defaults to 0 (no cap)
sophomoreCap : PositiveIntegerField
Maximum number of sophomores allowed to enroll
Defaults to 0 (no cap)
juniorCap : PositiveIntegerField
Maximum number of juniors allowed to enroll
Defaults to 0 (no cap)
seniorCap : PositiveIntegerField
Maximum number of seniors allowed to enroll
Defaults to 0 (no cap)
credits : IntegerField
Number of credits awarded for completing the course
Choices: 2 or 4 credits
Defaults to 4
prereqDescription : TextField, optional
Text description of prerequisites and requirements
Can be blank
recommended_courses : ManyToManyField to Course
Courses recommended but not required as prerequisites
Self-referential relationship
professor_override : BooleanField
Whether professors can override prerequisite requirements
Defaults to False
placement_course : ForeignKey to Course, optional
Reference to a placement test or course required
Self-referential relationship
corequisites : ManyToManyField to Course
Courses that must be taken concurrently
Self-referential, symmetrical relationship
professors : ManyToManyField to Professor
Professors teaching the course
sections : ReverseRelation via Section
Course sections with meeting times and locations
One-to-many relationship from Section model
fallOfferings : ManyToManyField to Year
Years the course is offered in fall semester
springOfferings : ManyToManyField to Year
Years the course is offered in spring semester
janOfferings : ManyToManyField to Year
Years the course is offered in January term
Methods
-------
__str__()
Returns the course name as a string
Examples
--------
>>> course = Course.objects.create(
... id=4140111,
... courseName="Introduction to Computer Science I",
... credits=4
... )
>>> course.departments.add(Department.objects.get(code="COSC"))
>>> course.courseCodes.create(value="COSC-111")
"""
id = models.IntegerField(
primary_key=True,
validators=[MinValueValidator(0), MaxValueValidator(9999999)],
help_text="Unique 7-digit course identifier. Format: XYYZCZZ where: X: College/semester (4=Amherst Fall, 5=Amherst Spring), YY: Department code (00-99), Z: Credit flag (1=half-credit, 0=full-credit), CZZ: Course number (000-999). Example: 4140112 = Fall COSC-112",
)
courseLink = models.URLField(
max_length=200,
blank=True,
null=True,
help_text="Direct URL to the course page on Amherst College's course catalog. Defaults to null if not provided.",
)
courseName = models.CharField(
max_length=200,
help_text="Full title of the course as it appears in the course catalog. Maximum length: 200 characters.",
)
courseCodes = models.ManyToManyField(
"CourseCode",
related_name="courses",
help_text="Official course codes/numbers (e.g., 'COSC-111', 'MATH-271'). Multiple codes possible for cross-listed courses.",
)
courseDescription = models.TextField(
blank=True,
help_text="Detailed description of course content, objectives, and requirements. Can be blank.",
)
courseMaterialsLink = models.URLField(
max_length=200,
blank=True,
null=True,
help_text="URL to course materials, syllabus, or additional resources. Defaults to null if not provided.",
)
keywords = models.ManyToManyField(
"Keyword",
related_name="courses",
help_text="Searchable keywords/tags associated with the course content.",
)
divisions = models.ManyToManyField(
"Division",
related_name="courses",
help_text="Academic divisions the course belongs to (e.g., 'Science', 'Humanities').",
)
departments = models.ManyToManyField(
"Department",
related_name="courses",
help_text="Academic departments offering the course.",
)
enrollmentText = models.TextField(
default="Contact the professor for enrollment details.",
help_text="Instructions for course enrollment and registration. Defaults to 'Contact the professor for enrollment details.'",
)
prefForMajor = models.BooleanField(
default=False,
help_text="Whether majors have enrollment priority. Defaults to False.",
)
overallCap = models.PositiveIntegerField(
default=0, help_text="Maximum total enrollment limit. Defaults to 0 (no cap)."
)
freshmanCap = models.PositiveIntegerField(
default=0,
help_text="Maximum number of freshmen allowed to enroll. Defaults to 0 (no cap).",
)
sophomoreCap = models.PositiveIntegerField(
default=0,
help_text="Maximum number of sophomores allowed to enroll. Defaults to 0 (no cap).",
)
juniorCap = models.PositiveIntegerField(
default=0,
help_text="Maximum number of juniors allowed to enroll. Defaults to 0 (no cap).",
)
seniorCap = models.PositiveIntegerField(
default=0,
help_text="Maximum number of seniors allowed to enroll. Defaults to 0 (no cap).",
)
credits = models.IntegerField(
default=4,
choices=[(2, "2 credits"), (4, "4 credits")],
help_text="Number of credits awarded for completing the course. Choices: 2 or 4 credits. Defaults to 4.",
)
prereqDescription = models.TextField(
blank=True,
help_text="Text description of prerequisites and requirements. Can be blank.",
)
recommended_courses = models.ManyToManyField(
"Course",
blank=True,
related_name="recommended_for",
help_text="Courses recommended but not required as prerequisites. Self-referential relationship.",
)
professor_override = models.BooleanField(
default=False,
help_text="Whether professors can override prerequisite requirements. Defaults to False.",
)
placement_course = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="placement_for",
help_text="Reference to a placement test or course required. Self-referential relationship.",
)
corequisites = models.ManyToManyField(
"self",
blank=True,
symmetrical=True,
help_text="Courses that must be taken concurrently. Self-referential, symmetrical relationship.",
)
professors = models.ManyToManyField(
"Professor", related_name="courses", help_text="Professors teaching the course."
)
fallOfferings = models.ManyToManyField(
"Year",
related_name="fOfferings",
blank=True,
help_text="Years the course is offered in fall semester.",
)
springOfferings = models.ManyToManyField(
"Year",
related_name="sOfferings",
blank=True,
help_text="Years the course is offered in spring semester.",
)
janOfferings = models.ManyToManyField(
"Year",
related_name="jOfferings",
blank=True,
help_text="Years the course is offered in January term.",
)
[docs]
def clean(self):
"""Validates that year-specific enrollment caps don't exceed overall cap"""
super().clean()
if self.overallCap > 0: # Only validate if overall cap is set
caps = {
"Freshman": self.freshmanCap,
"Sophomore": self.sophomoreCap,
"Junior": self.juniorCap,
"Senior": self.seniorCap,
}
for year, cap in caps.items():
if cap > self.overallCap:
raise ValidationError(
{
f"{year.lower()}Cap": f"{year} cap ({cap}) cannot exceed overall cap ({self.overallCap})"
}
)
[docs]
def __str__(self):
return self.courseName
[docs]
class CourseCode(models.Model):
"""
CourseCode model representing course codes/abbreviations.
Parameters
----------
value : CharField
The 8-9 character course code (e.g., "COSC-111", "CHEM-165L")
Must follow department code + hyphen + number format + (optional) letter suffix
"""
value = models.CharField(
max_length=9,
validators=[MinLengthValidator(8), MaxLengthValidator(9)],
help_text="The 8-9 character course code (e.g., 'COSC-111', 'CHEM-165L'). Must follow department code + hyphen + number format.",
)
def __str__(self):
return self.value
class Meta:
verbose_name = "Course Code"
verbose_name_plural = "Course Codes"
[docs]
class Department(models.Model):
"""
Department model representing academic departments at Amherst College.
Parameters
----------
name : CharField
The full name of the department (e.g., "Computer Science", "Biology")
code : CharField
The 4-letter department code (e.g., "COSC", "BCBP")
link : URLField
The link to the department page on amherst.edu
"""
name = models.CharField(
max_length=100,
help_text="The full name of the department (e.g., 'Computer Science', 'Biology')",
)
code = models.CharField(
max_length=4,
validators=[MinLengthValidator(4), MaxLengthValidator(4)],
help_text="The 4-letter department code (e.g., 'COSC', 'BCBP')",
)
link = models.URLField(
max_length=200, help_text="The link to the department page on amherst.edu"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Department"
verbose_name_plural = "Departments"
[docs]
class PrerequisiteSet(models.Model):
"""
PrerequisiteSet model representing a set of prerequisites for a course.
Parameters
----------
prerequisite_for : ForeignKey
Course this set of prerequisites is for
courses : ManyToManyField
One set of courses that can be completed to satisfy prerequisites
"""
prerequisite_for = models.ForeignKey(
"Course",
on_delete=models.CASCADE,
related_name="required_courses",
default=None,
help_text="Course this set of prerequisites is for",
)
courses = models.ManyToManyField(
"Course",
help_text="One set of courses that can be completed to satisfy prerequisites",
)
def __str__(self):
return ", ".join(
[str(course) for course in self.courses.all().select_related()]
)
class Meta:
verbose_name = "Prerequisite Set"
verbose_name_plural = "Prerequisite Sets"
[docs]
class Professor(models.Model):
"""
Professor model representing course instructors.
Parameters
----------
name : CharField
Full name of the professor
link : URLField
URL to professor's page on amherst.edu
"""
name = models.CharField(max_length=100, help_text="Full name of the professor")
link = models.URLField(
max_length=200, blank=True, null=True, help_text="Link to professor's page"
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Professor"
verbose_name_plural = "Professors"
ordering = ["name"]
[docs]
class Section(models.Model):
"""
Section model representing a specific course section with location, professor, and meeting time.
Parameters
----------
section_number: CharField
The section number (digits with optional letter suffix)
section_for : ForeignKey
Course this section is for
monday_start_time : TimeField
Start time for Monday meeting
monday_end_time : TimeField
End time for Monday meeting
tuesday_start_time : TimeField
Start time for Tuesday meeting
tuesday_end_time : TimeField
End time for Tuesday meeting
wednesday_start_time : TimeField
Start time for Wednesday meeting
wednesday_end_time : TimeField
End time for Wednesday meeting
thursday_start_time : TimeField
Start time for Thursday meeting
thursday_end_time : TimeField
End time for Thursday meeting
friday_start_time : TimeField
Start time for Friday meeting
friday_end_time : TimeField
End time for Friday meeting
saturday_start_time : TimeField
Start time for Saturday meeting
saturday_end_time : TimeField
End time for Saturday meeting
sunday_start_time : TimeField
Start time for Sunday meeting
sunday_end_time : TimeField
End time for Sunday meeting
location : CharField
Building and room number
professor : ForeignKey
Professor teaching this section
"""
section_number = models.CharField(
validators=[
RegexValidator(
regex=r"^\d{2}[A-Z]?$",
message="Section number must be 2 digits optionally followed by a capital letter",
code="invalid_section_number",
)
],
help_text="Section number (digits or L for lab)",
max_length=2,
default="1",
)
section_for = models.ForeignKey(
"Course",
on_delete=models.CASCADE,
related_name="sections",
default=None,
)
monday_start_time = models.TimeField(
null=True, help_text="Start time for Monday meeting"
)
monday_end_time = models.TimeField(
null=True, help_text="End time for Monday meeting"
)
tuesday_start_time = models.TimeField(
null=True, help_text="Start time for Tuesday meeting"
)
tuesday_end_time = models.TimeField(
null=True, help_text="End time for Tuesday meeting"
)
wednesday_start_time = models.TimeField(
null=True, help_text="Start time for Wednesday meeting"
)
wednesday_end_time = models.TimeField(
null=True, help_text="End time for Wednesday meeting"
)
thursday_start_time = models.TimeField(
null=True, help_text="Start time for Thursday meeting"
)
thursday_end_time = models.TimeField(
null=True, help_text="End time for Thursday meeting"
)
friday_start_time = models.TimeField(
null=True, help_text="Start time for Friday meeting"
)
friday_end_time = models.TimeField(
null=True, help_text="End time for Friday meeting"
)
saturday_start_time = models.TimeField(
null=True, help_text="Start time for Saturday meeting"
)
saturday_end_time = models.TimeField(
null=True, help_text="End time for Saturday meeting"
)
sunday_start_time = models.TimeField(
null=True, help_text="Start time for Sunday meeting"
)
sunday_end_time = models.TimeField(
null=True, help_text="End time for Sunday meeting"
)
location = models.CharField(max_length=100, help_text="Building and room number")
professor = models.ForeignKey(
"Professor", on_delete=models.CASCADE, related_name="sections"
)
[docs]
def clean(self):
super().clean()
# Check that start times are before end times for each day
time_pairs = [
("monday", self.monday_start_time, self.monday_end_time),
("tuesday", self.tuesday_start_time, self.tuesday_end_time),
("wednesday", self.wednesday_start_time, self.wednesday_end_time),
("thursday", self.thursday_start_time, self.thursday_end_time),
("friday", self.friday_start_time, self.friday_end_time),
("saturday", self.saturday_start_time, self.saturday_end_time),
("sunday", self.sunday_start_time, self.sunday_end_time),
]
errors = {}
for day, start_time, end_time in time_pairs:
if start_time and end_time: # Only validate if both times are set
if start_time >= end_time:
errors[f"{day}_start_time"] = (
f"{day.capitalize()} start time must be before end time"
)
if errors:
raise ValidationError(errors)
def __str__(self):
return f"{self.section_number} for {self.section_for}"
class Meta:
verbose_name = "Section"
verbose_name_plural = "Sections"
unique_together = ("section_for", "section_number")
[docs]
class Year(models.Model):
"""
Year model representing the academic year (specfic to each course).
Parameters
----------
id : IntegerField
Unique identifier for the academic year/link combo
year : IntegerField
Academic year (e.g., 2021)
link : URLField
URL link to course catalog
"""
id = models.AutoField(primary_key=True)
year = models.IntegerField(
validators=[MinValueValidator(1900)], help_text="Academic year (e.g., 2021)"
)
link = models.URLField(
max_length=200, help_text="Link to course catalog", null=True
)
def __str__(self):
return str(self.year)
class Meta:
verbose_name = "Year"
verbose_name_plural = "Years"
ordering = ["year"]
[docs]
class Keyword(models.Model):
"""
Keyword model representing keywords associated with courses.
Parameters
----------
name: CharField
The keyword value
"""
name = models.CharField(max_length=100, unique=True)
def __str__(self):
return self.name
class Meta:
verbose_name = "Keyword"
verbose_name_plural = "Keywords"
ordering = ["name"]
[docs]
class Division(models.Model):
"""
Division model representing academic divisions at Amherst College.
Parameters
----------
name : CharField
The full name of the division (e.g., "Science & Mathematics", "Social Sciences")
"""
name = models.CharField(
max_length=100,
help_text="The full name of the division (e.g., 'Science & Mathematics', 'Social Sciences')",
)
def __str__(self):
return self.name
class Meta:
verbose_name = "Division"
verbose_name_plural = "Divisions"
ordering = ["name"]