Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Department REST API #1877

Merged
merged 23 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions courses/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,20 @@ class Meta:
fields = ["name"]


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

courses = serializers.IntegerField()
programs = serializers.IntegerField()

class Meta:
model = models.Department
fields = DepartmentSerializer.Meta.fields + [
"courses",
"programs",
]


class CourseSerializer(BaseCourseSerializer):
"""Course model serializer"""

Expand Down
1 change: 1 addition & 0 deletions courses/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
v1.UserProgramEnrollmentsViewSet,
basename="user_program_enrollments_api",
)
router.register(r"departments", v1.DepartmentViewSet, basename="departments_api")

urlpatterns = [
re_path(r"^api/v1/", include(router.urls)),
Expand Down
16 changes: 15 additions & 1 deletion courses/views/v1/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType
from django.db import transaction
from django.db.models import Q
from django.db.models import Q, Count
from django.http import HttpResponse, HttpResponseRedirect
from django.urls import reverse
from django_filters.rest_framework import DjangoFilterBackend
Expand Down Expand Up @@ -33,6 +33,7 @@
PartnerSchool,
Program,
ProgramEnrollment,
Department,
)
from courses.serializers import (
CourseRunEnrollmentSerializer,
Expand All @@ -42,6 +43,7 @@
ProgramSerializer,
UserProgramEnrollmentDetailSerializer,
CourseWithCourseRunsSerializer,
DepartmentWithCountSerializer,
)
from courses.tasks import send_partner_school_email
from courses.utils import get_program_certificate_by_enrollment
Expand Down Expand Up @@ -507,3 +509,15 @@ def get_learner_record_from_uuid(request, uuid):
record.program, context={"user": record.user, "anonymous_pull": True}
).data
)


@permission_classes([])
class DepartmentViewSet(viewsets.ReadOnlyModelViewSet):
"""API view set for Departments"""

serializer_class = DepartmentWithCountSerializer

def get_queryset(self):
return Department.objects.annotate(
courses=Count("course"), programs=Count("program")
)
115 changes: 73 additions & 42 deletions frontend/public/src/containers/pages/CatalogPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ import {
programsQueryKey
} from "../../lib/queries/programs"

import {
departmentsSelector,
departmentsQuery,
departmentsQueryKey
} from "../../lib/queries/departments"

import { createStructuredSelector } from "reselect"
import { compose } from "redux"
import { connect } from "react-redux"
Expand Down Expand Up @@ -50,7 +56,8 @@ export class CatalogPage extends React.Component<Props> {
filteredCourses: [],
filteredPrograms: [],
filterCoursesCalled: false,
departments: [],
filteredDepartments: [],
filterDepartmentsCalled: false,
selectedDepartment: ALL_DEPARTMENTS,
mobileFilterWindowExpanded: false,
items_per_row: 3,
Expand Down Expand Up @@ -154,9 +161,11 @@ export class CatalogPage extends React.Component<Props> {
}

/**
* Updates the filteredCourses and courseDepartments state variables
* once coursesIsLoading is False. Adds an observer to detect when
* Updates the filteredCourses state variable
* once coursesIsLoading is false.. Adds an observer to detect when
* the learner has scrolled to the bottom of the visible catalog items.
* Updates the filteredDepartments state variable once departmentsIsLoading
* is false.
*/
componentDidUpdate = () => {
const { courses, coursesIsLoading } = this.props
Expand All @@ -168,9 +177,6 @@ export class CatalogPage extends React.Component<Props> {
courses
)
this.setState({ filteredCourses: filteredCourses })
this.setState({
departments: this.collectDepartmentsFromCatalogItems(filteredCourses)
})

// Detect when the bottom of the catalog page has been reached and display more catalog items.
this.io = new window.IntersectionObserver(
Expand All @@ -179,30 +185,54 @@ export class CatalogPage extends React.Component<Props> {
)
this.io.observe(this.container.current)
}

if (
!this.props.departmentsIsLoading &&
!this.state.filterDepartmentsCalled
) {
this.setState({ filterDepartmentsCalled: true })
this.setState({
filteredDepartments: this.filterDepartmentsByTabName(
this.state.tabSelected
)
})
}
}

/**
* Returns an array of unique Department names that are associated with the Courses or Programs in the
* parameter and also includes an entry for "All Departments".
* @param {Array<CourseDetailWithRuns | Program>} catalogItems Array of Courses or Programs to collect Department names from.
* Returns an array of departments names which have one or more course(s) or program(s)
* related to them depending on the currently selected tab.
* @param {string} selectedTabName the name of the currently selected tab.
*/
collectDepartmentsFromCatalogItems(
catalogItems: Array<CourseDetailWithRuns | Program>
) {
const departments = Array.from(catalogItems, item => item.departments)
return [
...new Set([
ALL_DEPARTMENTS,
...departments.flat().map(department => department.name)
])
]
filterDepartmentsByTabName(selectedTabName: string) {
if (!this.props.departmentsIsLoading) {
const { departments } = this.props
if (selectedTabName === COURSES_TAB) {
return [
...new Set([
ALL_DEPARTMENTS,
...departments.flatMap(department =>
department.courses > 0 ? department.name : []
)
])
]
} else {
return [
...new Set([
ALL_DEPARTMENTS,
...departments.flatMap(department =>
department.programs > 0 ? department.name : []
)
])
]
}
}
}

/**
* Updates this.state.selectedDepartment to {ALL_DEPARTMENTS},
* updates this.state.tabSelected to the parameter,
* updates this.state.numberCatalogRowsToDisplay to {DEFAULT_MIN_CATALOG_ROWS_RENDERED},
* updates this.state.departments to equal the unique department
* updates this.state.filteredDepartments
* names from the catalog items in the selected tab,
* updates this.state.filteredPrograms to equal the programs
* which meet the criteria to be displayed in the catalog.
Expand All @@ -212,16 +242,7 @@ export class CatalogPage extends React.Component<Props> {
this.setState({ tabSelected: selectTabName })
this.changeSelectedDepartment(ALL_DEPARTMENTS, selectTabName)

if (selectTabName === COURSES_TAB) {
const { coursesIsLoading } = this.props
if (!coursesIsLoading) {
this.setState({
departments: this.collectDepartmentsFromCatalogItems(
this.state.allCoursesRetrieved
)
})
}
} else {
if (selectTabName === PROGRAMS_TAB) {
const { programs, programsIsLoading } = this.props
if (!programsIsLoading) {
const programsToFilter = []
Expand All @@ -241,11 +262,11 @@ export class CatalogPage extends React.Component<Props> {
this.setState({
filteredPrograms: filteredPrograms
})
this.setState({
departments: this.collectDepartmentsFromCatalogItems(programsToFilter)
})
}
}
this.setState({
filteredDepartments: this.filterDepartmentsByTabName(selectTabName)
})
}

/**
Expand Down Expand Up @@ -504,7 +525,7 @@ export class CatalogPage extends React.Component<Props> {
*/
renderDepartmentSideBarList() {
const departmentSideBarListItems = []
this.state.departments.forEach(department =>
this.state.filteredDepartments.forEach(department =>
departmentSideBarListItems.push(
<li
className={`sidebar-link ${
Expand Down Expand Up @@ -664,20 +685,30 @@ const getNextProgramPage = page =>
force: true
})

const mapPropsToConfig = () => [coursesQuery(1), programsQuery(1)]
const mapPropsToConfig = () => [
coursesQuery(1),
programsQuery(1),
departmentsQuery()
]

const mapDispatchToProps = {
getNextCoursePage: getNextCoursePage,
getNextProgramPage: getNextProgramPage
}

const mapStateToProps = createStructuredSelector({
courses: coursesSelector,
coursesNextPage: coursesNextPageSelector,
programs: programsSelector,
programsNextPage: programsNextPageSelector,
coursesIsLoading: pathOr(true, ["queries", coursesQueryKey, "isPending"]),
programsIsLoading: pathOr(true, ["queries", programsQueryKey, "isPending"])
courses: coursesSelector,
coursesNextPage: coursesNextPageSelector,
programs: programsSelector,
programsNextPage: programsNextPageSelector,
departments: departmentsSelector,
coursesIsLoading: pathOr(true, ["queries", coursesQueryKey, "isPending"]),
programsIsLoading: pathOr(true, ["queries", programsQueryKey, "isPending"]),
departmentsIsLoading: pathOr(true, [
"queries",
departmentsQueryKey,
"isPending"
])
})

export default compose(
Expand Down
Loading