Source code for timetable.views
# -*- 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 itertools
import logging
from django.shortcuts import get_object_or_404
from hashids import Hashids
from rest_framework import status
from rest_framework.response import Response
from rest_framework.views import APIView
from analytics.models import SharedTimetable
from analytics.views import save_analytics_timetable
from courses.serializers import CourseSerializer
from student.utils import get_student
from timetable.serializers import DisplayTimetableSerializer
from timetable.models import Semester, Course, Section
from timetable.utils import (
update_locked_sections,
courses_to_timetables,
)
from helpers.mixins import ValidateSubdomainMixin, FeatureFlowView, CsrfExemptMixin
from semesterly.settings import get_secret
hashids = Hashids(salt=get_secret("HASHING_SALT"))
logger = logging.getLogger(__name__)
[docs]class TimetableView(CsrfExemptMixin, ValidateSubdomainMixin, APIView):
"""
This view is responsible for responding to any requests dealing with the
generation of timetables and the satisfaction of constraints provided by
the frontend/user.
"""
[docs] def post(self, request):
"""Generate best timetables given the user's selected courses"""
school = request.subdomain
params = request.data
student = get_student(request)
course_ids = list(params["courseSections"].keys())
courses = [Course.objects.get(id=cid) for cid in course_ids]
locked_sections = params["courseSections"]
self.set_params_semester(params)
save_analytics_timetable(
courses, params["semester"], school, get_student(request)
)
self.update_courses_and_locked_sections(
params, course_ids, courses, locked_sections
)
opt_course_ids = params.get("optionCourses", [])
max_optional = params.get("numOptionCourses", len(opt_course_ids))
optional_courses = [Course.objects.get(id=cid) for cid in opt_course_ids]
optional_course_subsets = [
subset
for subset_size in range(max_optional, -1, -1)
for subset in itertools.combinations(optional_courses, subset_size)
]
custom_events = params.get("customSlots", [])
preferences = params["preferences"]
with_conflicts = preferences.get("tryWithConflicts", False)
show_weekend = preferences.get("showWeekend", True)
timetables = [
timetable
for opt_courses in optional_course_subsets
for timetable in courses_to_timetables(
courses + list(opt_courses),
locked_sections,
params["semester"],
params["school"],
custom_events,
with_conflicts,
opt_course_ids,
show_weekend,
)
]
context = self.create_context(request, params, student)
courses = list(courses + optional_courses)
response = self.create_response(courses, locked_sections, timetables, context)
return Response(response, status=status.HTTP_200_OK)
def update_courses_and_locked_sections(
self, params, course_ids, courses, locked_sections
):
for updated_course in params.get("updated_courses", []):
cid = str(updated_course["course_id"])
locked_sections.setdefault(cid, {})
if cid not in course_ids:
courses.append(Course.objects.get(id=int(cid)))
for locked_section in filter(bool, updated_course["section_codes"]):
update_locked_sections(
locked_sections, cid, locked_section, params["semester"]
)
def set_params_semester(self, params):
try:
params["semester"] = Semester.objects.get_or_create(**params["semester"])[0]
except TypeError: # handle deprecated cached semesters from frontend
params["semester"] = (
Semester.objects.get(name="Fall", year="2016")
if params["semester"] == "F"
else Semester.objects.get(name="Spring", year="2017")
)
def create_response(self, courses, locked_sections, timetables, context):
return {
"timetables": DisplayTimetableSerializer(timetables, many=True).data,
"new_c_to_s": locked_sections,
"courses": CourseSerializer(courses, context=context, many=True).data,
}
def create_context(self, request, params, student):
return {
"semester": params["semester"],
"school": request.subdomain,
"student": student,
}
[docs]class TimetableLinkView(FeatureFlowView):
"""
A subclass of :obj:`FeatureFlowView` (see :ref:`flows`) for the
viewing of shared timetable links. Provides the logic for preloading
the shared timetable into initData when a user hits the corresponding
url. The frontend can then act on this data to load the shared timetable
for viewing.
Additionally, on POST provides the functionality for the creation of
shared timetables.
"""
feature_name = "SHARE_TIMETABLE"
[docs] def get_feature_flow(self, request, slug):
"""
Overrides :obj:`FeatureFlowView` *get_feature_flow* method. Takes the slug,
decrypts the hashed database id, and either retrieves the corresponding
timetable or hits a 404.
"""
timetable_id = hashids.decrypt(slug)[0]
shared_timetable = get_object_or_404(
SharedTimetable, id=timetable_id, school=request.subdomain
)
context = {
"semester": shared_timetable.semester,
"school": request.subdomain,
"student": get_student(request),
}
return {
"semester": shared_timetable.semester,
"courses": CourseSerializer(
shared_timetable.courses, context=context, many=True
).data,
"sharedTimetable": DisplayTimetableSerializer.from_model(
shared_timetable
).data,
}
[docs] def post(self, request):
"""
Creates a :obj:`SharedTimetable` and returns the hashed database id
as the slug for the url which students then share and access.
"""
school = request.subdomain
timetable = request.data["timetable"]
has_conflict = timetable.get("has_conflict", False)
semester, _ = Semester.objects.get_or_create(**request.data["semester"])
student = get_student(request)
shared_timetable = SharedTimetable.objects.create(
student=student, school=school, semester=semester, has_conflict=has_conflict
)
shared_timetable.save()
self.save_courses(timetable, shared_timetable)
response = {"slug": hashids.encrypt(shared_timetable.id)}
return Response(response, status=status.HTTP_200_OK)
def save_courses(self, timetable: dict, shared_timetable: SharedTimetable):
added_courses = set()
for slot in timetable["slots"]:
course_id, section_id = slot["course"], slot["section"]
if course_id not in added_courses:
course_obj = Course.objects.get(id=course_id)
shared_timetable.courses.add(course_obj)
added_courses.add(course_id)
section_obj = Section.objects.get(id=section_id)
shared_timetable.sections.add(section_obj)
if section_obj.course.id not in added_courses:
return Response(status=status.HTTP_400_BAD_REQUEST)
shared_timetable.save()