# 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 re
from django.forms import model_to_dict
from django.db import models
from rest_framework import serializers
from timetable.models import Course, Section, Evaluation, CourseIntegration, Integration, Semester
import utils
class EvaluationSerializer(serializers.ModelSerializer):
class Meta:
model = Evaluation
fields = '__all__'
[docs]class CourseSerializer(serializers.ModelSerializer):
"""
Serialize a Course into a dictionary with detailed information about the course, and all
related entities (eg Sections). Used for search results and course modals.
Takes a context with parameters:
school: str (required)
semester: Semester (required)
student: Student (optional)
"""
evals = serializers.SerializerMethodField()
integrations = serializers.SerializerMethodField()
related_courses = serializers.SerializerMethodField()
reactions = serializers.SerializerMethodField()
textbooks = serializers.SerializerMethodField()
regexed_courses = serializers.SerializerMethodField()
popularity_percent = serializers.SerializerMethodField()
is_waitlist_only = serializers.SerializerMethodField()
sections = serializers.SerializerMethodField()
[docs] def get_evals(self, course):
"""
Flag all eval instances s.t. there exists repeated term+year values.
Return:
List of modified evaluation dictionaries (added flag 'unique_term_year')
"""
evals = map(model_to_dict, Evaluation.objects.filter(course=course).order_by('year'))
years = Evaluation.objects.filter(course=course).values('year').annotate(models.Count('id')) \
.filter(id__count__gt=1).values_list('year')
years = {e[0] for e in years}
for course_eval in evals:
course_eval['unique_term_year'] = not course_eval['year'] in years
return evals
def get_integrations(self, course):
ids = CourseIntegration.objects.filter(course__id=course.id).values_list("integration",
flat=True)
return list(Integration.objects.filter(id__in=ids).values_list("name", flat=True))
# TODO: use course serializer but only recurse one level
def get_related_courses(self, course):
info = []
related = course.related_courses.filter(
section__semester=self.context['semester']).distinct()[:5]
for course in related:
info.append(model_to_dict(course, exclude=['related_courses', 'unstopped_description']))
return info
def get_reactions(self, course):
return course.get_reactions(self.context.get('student'))
def get_textbooks(self, course):
sections = course.section_set.filter(semester=self.context['semester'])
return {section.meeting_section: section.get_textbooks() for section in sections}
[docs] def get_regexed_courses(self, course):
"""
Given course data, search for all occurrences of a course code in the course description and
prereq info and return a map from course code to course name for each course code.
"""
school_to_course_regex = {
'jhu': r'([A-Z]{2}\.\d{3}\.\d{3})',
'uoft': r'([A-Z]{3}[A-Z0-9]\d{2}[HY]\d)',
'vandy': r'([A-Z-&]{2,7}\s\d{4}[W]?)',
'gw': r'([A-Z]{2,5}\s\d{4}[W]?)',
'umich': r'([A-Z]{2,8}\s\d{3})',
'chapman': r'([A-Z]{2,4}\s\d{3})',
'salisbury': r'([A-Z]{3,4} \d{2,3})',
}
course_code_to_name = {}
if self.context['school'] in school_to_course_regex:
course_code_matches = re.findall(school_to_course_regex[self.context['school']],
course.description + course.prerequisites)
# TODO: get all course objects in one db access
for course_code in course_code_matches:
try:
course = Course.objects.get(school=self.context['school'],
code__icontains=course_code)
course_code_to_name[course_code] = course.name
except (Course.DoesNotExist, Course.MultipleObjectsReturned):
pass
return course_code_to_name
[docs] def get_popularity_percent(self, course):
""" Return percentage of course capacity that is filled by registered students. """
tts_with_course = course.personaltimetable_set.filter(semester=self.context['semester'])
num_students_in_course = tts_with_course.values('student').distinct().count()
sections = course.section_set.filter(semester=self.context['semester'])
course_capacity = sum(sections.values_list('size', flat=True)) if sections else 0
return num_students_in_course / float(course_capacity) if course_capacity else 0
def get_is_waitlist_only(self, course):
return utils.is_waitlist_only(course, self.context['semester'])
def get_sections(self, course):
return [SectionSerializer(section).data
for section in course.section_set.filter(semester=self.context['semester'])]
class Meta:
model = Course
fields = (
'code',
'name',
'id',
'description',
'department',
'num_credits',
'areas',
'campus',
'evals',
'integrations',
'related_courses',
'reactions',
'textbooks',
'regexed_courses',
'popularity_percent',
'sections',
'prerequisites',
'exclusions',
'corequisites',
'areas',
'is_waitlist_only'
)
class SectionSerializer(serializers.ModelSerializer):
class Meta:
model = Section
fields = (
'id',
'meeting_section',
'size',
'enrolment',
'waitlist',
'waitlist_size',
'section_type',
'instructors',
'semester',
'offering_set'
)
depth = 1 # also serializer offerings
class SemesterSerializer(serializers.ModelSerializer):
class Meta:
fields = '__all__'
model = Semester
[docs]def get_section_dict(section):
""" Returns a dictionary of a section including indicator of whether that section is filled """
section_data = model_to_dict(section)
section_data['is_section_filled'] = section.is_full()
return section_data