Skip to content

Commit

Permalink
3689: add department filter to the api endpoints for courses and prog…
Browse files Browse the repository at this point in the history
…rams (#2118)

* Working hold

* Black formatting

* Repair views test

* Code review comments

* lint
  • Loading branch information
collinpreston authored Mar 4, 2024
1 parent 8a0cf70 commit 26fa0c5
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 71 deletions.
45 changes: 34 additions & 11 deletions courses/serializers/v2/departments.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from django.db.models import Prefetch, Q, Count
from django.db.models import Q
from mitol.common.utils import now_in_utc
from rest_framework import serializers

Expand All @@ -13,13 +13,25 @@ class Meta:
fields = ["id", "name"]


class DepartmentWithCountSerializer(DepartmentSerializer):
class DepartmentWithCoursesAndProgramsSerializer(DepartmentSerializer):
"""Department model serializer that includes the number of courses and programs associated with each"""

courses = serializers.SerializerMethodField()
programs = serializers.SerializerMethodField()
course_ids = serializers.SerializerMethodField()
program_ids = serializers.SerializerMethodField()

def get_courses(self, instance):
def get_course_ids(self, instance):
"""
Returns a list of course IDs associated with courses which are live and
have a CMS page that is live. The associated course runs must be live,
have a start date, enrollment start date in the past, and enrollment end
date in the future or not defined.
Args:
instance (courses.models.Department): Department model instance.
Returns:
list: Course IDs associated with the Department.
"""
now = now_in_utc()
related_courses = instance.course_set.filter(live=True, page__live=True)
relevant_courseruns = CourseRun.objects.filter(
Expand All @@ -29,21 +41,32 @@ def get_courses(self, instance):
& Q(enrollment_start__lt=now)
& (Q(enrollment_end=None) | Q(enrollment_end__gt=now))
).values_list("id", flat=True)

return (
related_courses.filter(courseruns__id__in=relevant_courseruns)
.distinct()
.count()
.values_list("id", flat=True)
)

def get_programs(self, instance):
def get_program_ids(self, instance):
"""
Returns a list of program IDs associated with the department
if the program is live and has a live CMS page.
Args:
instance (courses.models.Department): Department model instance.
Returns:
list: Program IDs associated with the Department.
"""
return (
instance.program_set.filter(live=True, page__live=True).distinct().count()
instance.program_set.filter(live=True, page__live=True)
.distinct()
.values_list("id", flat=True)
)

class Meta:
model = Department
fields = DepartmentSerializer.Meta.fields + [
"courses",
"programs",
"course_ids",
"program_ids",
]
46 changes: 30 additions & 16 deletions courses/serializers/v2/departments_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from datetime import timedelta

import pytest

from courses.factories import (
Expand All @@ -10,7 +8,7 @@
)
from courses.serializers.v2.departments import (
DepartmentSerializer,
DepartmentWithCountSerializer,
DepartmentWithCoursesAndProgramsSerializer,
)

from main.test_utils import assert_drf_json_equal
Expand All @@ -26,19 +24,33 @@ def test_serialize_department(mock_context):
# Should return 0 when there are no courses or programs at all, or when there are, but none are relevant
def test_serialize_department_with_courses_and_programs__no_related(mock_context):
department = DepartmentFactory.create()
data = DepartmentWithCountSerializer(instance=department, context=mock_context).data
data = DepartmentWithCoursesAndProgramsSerializer(
instance=department, context=mock_context
).data
assert_drf_json_equal(
data,
{"id": department.id, "name": department.name, "courses": 0, "programs": 0},
{
"id": department.id,
"name": department.name,
"course_ids": [],
"program_ids": [],
},
)

course = CourseFactory.create()
CourseRunFactory.create(course=course, in_future=True)
ProgramFactory.create()
data = DepartmentWithCountSerializer(instance=department, context=mock_context).data
data = DepartmentWithCoursesAndProgramsSerializer(
instance=department, context=mock_context
).data
assert_drf_json_equal(
data,
{"id": department.id, "name": department.name, "courses": 0, "programs": 0},
{
"id": department.id,
"name": department.name,
"course_ids": [],
"program_ids": [],
},
)


Expand All @@ -54,8 +66,8 @@ def test_serialize_department_with_courses_and_programs__with_multiples(
invalid_programs,
):
department = DepartmentFactory.create()
valid_courses_list = []
valid_programs_list = []
valid_course_id_list = []
valid_program_id_list = []

invalid_courses_list = []
invalid_programs_list = []
Expand All @@ -66,25 +78,27 @@ def test_serialize_department_with_courses_and_programs__with_multiples(
# Each course has 2 course runs that are possible matches to make sure it is not returned twice.
CourseRunFactory.create(course=course, in_future=True)
CourseRunFactory.create(course=course, in_progress=True)
valid_courses_list += [course]
valid_course_id_list.append(course.id)
vc -= 1
vp = valid_programs
while vp > 0:
valid_programs_list += [ProgramFactory.create(departments=[department])]
valid_program_id_list.append(ProgramFactory.create(departments=[department]).id)
vp -= 1
while invalid_courses > 0:
invalid_courses_list += [CourseFactory.create()]
# invalid_courses_list += [CourseFactory.create()]
invalid_courses -= 1
while invalid_programs > 0:
invalid_programs_list += [ProgramFactory.create()]
# invalid_programs_list += [ProgramFactory.create()]
invalid_programs -= 1
data = DepartmentWithCountSerializer(instance=department, context=mock_context).data
data = DepartmentWithCoursesAndProgramsSerializer(
instance=department, context=mock_context
).data
assert_drf_json_equal(
data,
{
"id": department.id,
"name": department.name,
"courses": valid_courses,
"programs": valid_programs,
"course_ids": valid_course_id_list,
"program_ids": valid_program_id_list,
},
)
7 changes: 5 additions & 2 deletions courses/views/v2/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Course API Views version 2
"""

from rest_framework import viewsets
from rest_framework.pagination import PageNumberPagination
import django_filters
Expand All @@ -18,7 +19,9 @@
from courses.serializers.v2.courses import (
CourseWithCourseRunsSerializer,
)
from courses.serializers.v2.departments import DepartmentWithCountSerializer
from courses.serializers.v2.departments import (
DepartmentWithCoursesAndProgramsSerializer,
)


class Pagination(PageNumberPagination):
Expand Down Expand Up @@ -130,7 +133,7 @@ def get_serializer_context(self):
class DepartmentViewSet(viewsets.ReadOnlyModelViewSet):
"""API view set for Departments"""

serializer_class = DepartmentWithCountSerializer
serializer_class = DepartmentWithCoursesAndProgramsSerializer
pagination_class = Pagination
permission_classes = []

Expand Down
9 changes: 6 additions & 3 deletions courses/views/v2/views_test.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Tests for courses api views v2
"""

import logging
import random

Expand All @@ -12,7 +13,9 @@
from courses.factories import DepartmentFactory
from courses.models import Course, Program
from courses.serializers.v2.courses import CourseWithCourseRunsSerializer
from courses.serializers.v2.departments import DepartmentWithCountSerializer
from courses.serializers.v2.departments import (
DepartmentWithCoursesAndProgramsSerializer,
)
from courses.serializers.v2.programs import ProgramSerializer
from courses.views.test_utils import (
num_queries_from_course,
Expand All @@ -37,7 +40,7 @@ def test_get_departments(
empty_departments_from_fixture = []
for department in departments:
empty_departments_from_fixture.append(
DepartmentWithCountSerializer(
DepartmentWithCoursesAndProgramsSerializer(
instance=department, context=mock_context
).data
)
Expand Down Expand Up @@ -65,7 +68,7 @@ def test_get_departments(
departments_from_fixture = []
for department in departments:
departments_from_fixture.append(
DepartmentWithCountSerializer(
DepartmentWithCoursesAndProgramsSerializer(
instance=department, context=mock_context
).data
)
Expand Down
15 changes: 8 additions & 7 deletions frontend/public/src/containers/pages/CatalogPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ export class CatalogPage extends React.Component<Props> {
...new Set([
ALL_DEPARTMENTS,
...departments.flatMap(department =>
department.courses > 0 ? department.name : []
department.course_ids.length > 0 ? department.name : []
)
])
]
Expand All @@ -233,7 +233,7 @@ export class CatalogPage extends React.Component<Props> {
...new Set([
ALL_DEPARTMENTS,
...departments.flatMap(department =>
department.programs > 0 ? department.name : []
department.program_ids.length > 0 ? department.name : []
)
])
]
Expand Down Expand Up @@ -311,9 +311,10 @@ export class CatalogPage extends React.Component<Props> {
}

/**
* Returns a filtered array of Course Runs which are live, define a start date,
* enrollment start date is before the current date and time, and
* enrollment end date is not defined or is after the current date and time.
* Returns a filtered array of Course Runs which are live and:
* - Have a start_date before the current date and time
* - Have an enrollment_start_date that is before the current date and time
* - Has an enrollment_end_date that is not defined or is after the current date and time.
* @param {Array<BaseCourseRun>} courseRuns The array of Course Runs apply the filter to.
*/
validateCoursesCourseRuns(courseRuns: Array<BaseCourseRun>) {
Expand Down Expand Up @@ -379,7 +380,7 @@ export class CatalogPage extends React.Component<Props> {
} else if (this.state.selectedDepartment !== ALL_DEPARTMENTS) {
return departments.find(
department => department.name === this.state.selectedDepartment
).courses
).course_ids.length
}
}

Expand All @@ -390,7 +391,7 @@ export class CatalogPage extends React.Component<Props> {
} else if (this.state.selectedDepartment !== ALL_DEPARTMENTS) {
return departments.find(
department => department.name === this.state.selectedDepartment
).programs
).program_ids.length
} else return 0
}

Expand Down
60 changes: 30 additions & 30 deletions frontend/public/src/containers/pages/CatalogPage_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,14 @@ describe("CatalogPage", function() {
},
departments: [
{
name: "History",
courses: 0,
programs: 5
name: "History",
course_ids: [],
program_ids: [1, 2, 3, 4, 5]
},
{
name: "Science",
courses: 1,
programs: 0
name: "Science",
course_ids: [2],
program_ids: []
}
]
}
Expand Down Expand Up @@ -262,24 +262,24 @@ describe("CatalogPage", function() {
entities: {
departments: [
{
name: "department1",
courses: 1,
programs: 0
name: "department1",
course_ids: [1],
program_ids: []
},
{
name: "department2",
courses: 1,
programs: 1
name: "department2",
course_ids: [1],
program_ids: [1]
},
{
name: "department3",
courses: 0,
programs: 1
name: "department3",
course_ids: [],
program_ids: [1]
},
{
name: "department4",
courses: 0,
programs: 0
name: "department4",
course_ids: [],
program_ids: []
}
]
}
Expand Down Expand Up @@ -522,19 +522,19 @@ describe("CatalogPage", function() {
},
departments: [
{
name: "History",
courses: 2,
programs: 1
name: "History",
course_ids: [1, 2],
program_ids: [1]
},
{
name: "Math",
courses: 3,
programs: 0
name: "Math",
course_ids: [1, 2, 3],
program_ids: []
},
{
name: "department4",
courses: 0,
programs: 0
name: "department4",
course_ids: [],
program_ids: []
}
]
}
Expand Down Expand Up @@ -799,9 +799,9 @@ describe("CatalogPage", function() {
},
departments: [
{
name: "History",
courses: 1,
programs: 1
name: "History",
course_ids: [1],
program_ids: [1]
}
]
}
Expand Down
Loading

0 comments on commit 26fa0c5

Please sign in to comment.