From 0a8d37ce08039e01044f85f69717fbf122cad5cf Mon Sep 17 00:00:00 2001 From: jenniw Date: Mon, 1 Apr 2024 16:17:31 -0400 Subject: [PATCH] Add URL routing for catalog for courses/programs and department (#2140) * add to urls.js * update for tab/dept * take state out of constructor * adding slug field * slug migration and adding to management command * fixing routing with new slug stuff and routes in order * successful introduction of slug to the page * FIxed routing to further pages and fake pages * tests and formatting * Test Fix * fixing migrations * fixed js test finally * well that was silly --- courses/admin.py | 1 + .../management/commands/create_courseware.py | 6 +- .../migrations/0048_add_department_slug.py | 20 + .../0049_populate_department_slug.py | 22 + .../0050_make_department_slug_unique.py | 18 + courses/models.py | 7 + courses/serializers/v2/departments.py | 2 +- courses/serializers/v2/departments_test.py | 15 +- frontend/public/src/containers/App.js | 8 + .../src/containers/pages/CatalogPage.js | 143 +++++-- .../src/containers/pages/CatalogPage_test.js | 392 ++++++++++++++---- frontend/public/src/lib/urls.js | 16 +- 12 files changed, 519 insertions(+), 131 deletions(-) create mode 100644 courses/migrations/0048_add_department_slug.py create mode 100644 courses/migrations/0049_populate_department_slug.py create mode 100644 courses/migrations/0050_make_department_slug_unique.py diff --git a/courses/admin.py b/courses/admin.py index 954245300..b88b08cf2 100644 --- a/courses/admin.py +++ b/courses/admin.py @@ -382,6 +382,7 @@ class DepartmentAdmin(admin.ModelAdmin): """Admin for Department""" model = Department + list_display = ("name", "slug") class BlockedCountryAdmin(TimestampedModelAdmin): diff --git a/courses/management/commands/create_courseware.py b/courses/management/commands/create_courseware.py index 9ad05a6ab..5b7934497 100644 --- a/courses/management/commands/create_courseware.py +++ b/courses/management/commands/create_courseware.py @@ -3,11 +3,11 @@ a course run). """ -from typing import Union -from django.db import models -from typing import List +from typing import List, Union from django.core.management import BaseCommand +from django.db import models +from django.utils.text import slugify from courses.models import Course, CourseRun, Department, Program from main.utils import parse_supplied_date diff --git a/courses/migrations/0048_add_department_slug.py b/courses/migrations/0048_add_department_slug.py new file mode 100644 index 000000000..fd0df1df3 --- /dev/null +++ b/courses/migrations/0048_add_department_slug.py @@ -0,0 +1,20 @@ +# Generated by Django 3.2.23 on 2024-03-27 14:49 + +from django.db import migrations, models + + +# Running this migration across 3 files to support the unique field, per django docs: +# https://docs.djangoproject.com/en/4.2/howto/writing-migrations/#migrations-that-add-unique-fields +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0047_courses_and_programs_department_required"), + ] + + operations = [ + migrations.AddField( + model_name="department", + name="slug", + field=models.SlugField(max_length=255, null=True), + ), + ] diff --git a/courses/migrations/0049_populate_department_slug.py b/courses/migrations/0049_populate_department_slug.py new file mode 100644 index 000000000..d3ee44029 --- /dev/null +++ b/courses/migrations/0049_populate_department_slug.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.23 on 2024-03-27 15:35 + +from django.db import migrations +from django.utils.text import slugify + + +def generate_slug(apps, schema): + Department = apps.get_model("courses", "Department") + for department in Department.objects.all(): + department.slug = slugify(department.name) + department.save(update_fields=["slug"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0048_add_department_slug"), + ] + + operations = [ + migrations.RunPython(generate_slug, reverse_code=migrations.RunPython.noop), + ] diff --git a/courses/migrations/0050_make_department_slug_unique.py b/courses/migrations/0050_make_department_slug_unique.py new file mode 100644 index 000000000..b295c9721 --- /dev/null +++ b/courses/migrations/0050_make_department_slug_unique.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2024-03-27 15:35 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("courses", "0049_populate_department_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="department", + name="slug", + field=models.SlugField(max_length=128, unique=True), + ), + ] diff --git a/courses/models.py b/courses/models.py index d66453975..ec1538921 100644 --- a/courses/models.py +++ b/courses/models.py @@ -15,6 +15,7 @@ from django.db.models import Q from django.db.models.constraints import CheckConstraint, UniqueConstraint from django.utils.functional import cached_property +from django.utils.text import slugify from django_countries.fields import CountryField from mitol.common.models import TimestampedModel from mitol.common.utils.collections import first_matching_item @@ -110,10 +111,16 @@ class Department(TimestampedModel): """ name = models.CharField(max_length=128, unique=True) + slug = models.SlugField(max_length=128, unique=True) def __str__(self): return self.name + def save(self, *args, **kwargs): + if not self.slug: + self.slug = slugify(self.name) + super().save(*args, **kwargs) + class Program(TimestampedModel, ValidateOnSaveMixin): """Model for a course program""" diff --git a/courses/serializers/v2/departments.py b/courses/serializers/v2/departments.py index 5cdf5568f..66447c02d 100644 --- a/courses/serializers/v2/departments.py +++ b/courses/serializers/v2/departments.py @@ -10,7 +10,7 @@ class DepartmentSerializer(serializers.ModelSerializer): class Meta: model = Department - fields = ["id", "name"] + fields = ["id", "name", "slug"] class DepartmentWithCoursesAndProgramsSerializer(DepartmentSerializer): diff --git a/courses/serializers/v2/departments_test.py b/courses/serializers/v2/departments_test.py index c8c5e2279..69e079943 100644 --- a/courses/serializers/v2/departments_test.py +++ b/courses/serializers/v2/departments_test.py @@ -3,14 +3,13 @@ from courses.factories import ( CourseFactory, CourseRunFactory, - ProgramFactory, DepartmentFactory, + ProgramFactory, ) from courses.serializers.v2.departments import ( DepartmentSerializer, DepartmentWithCoursesAndProgramsSerializer, ) - from main.test_utils import assert_drf_json_equal @@ -18,7 +17,9 @@ def test_serialize_department(mock_context): department = DepartmentFactory.create() data = DepartmentSerializer(instance=department, context=mock_context).data - assert_drf_json_equal(data, {"id": department.id, "name": department.name}) + assert_drf_json_equal( + data, {"id": department.id, "name": department.name, "slug": department.slug} + ) # Should return 0 when there are no courses or programs at all, or when there are, but none are relevant @@ -34,6 +35,7 @@ def test_serialize_department_with_courses_and_programs__no_related(mock_context "name": department.name, "course_ids": [], "program_ids": [], + "slug": department.slug, }, ) @@ -50,6 +52,7 @@ def test_serialize_department_with_courses_and_programs__no_related(mock_context "name": department.name, "course_ids": [], "program_ids": [], + "slug": department.slug, }, ) @@ -69,9 +72,6 @@ def test_serialize_department_with_courses_and_programs__with_multiples( valid_course_id_list = [] valid_program_id_list = [] - invalid_courses_list = [] - invalid_programs_list = [] - vc = valid_courses while vc > 0: course = CourseFactory.create(departments=[department]) @@ -85,10 +85,8 @@ def test_serialize_department_with_courses_and_programs__with_multiples( valid_program_id_list.append(ProgramFactory.create(departments=[department]).id) vp -= 1 while invalid_courses > 0: - # invalid_courses_list += [CourseFactory.create()] invalid_courses -= 1 while invalid_programs > 0: - # invalid_programs_list += [ProgramFactory.create()] invalid_programs -= 1 data = DepartmentWithCoursesAndProgramsSerializer( instance=department, context=mock_context @@ -100,5 +98,6 @@ def test_serialize_department_with_courses_and_programs__with_multiples( "name": department.name, "course_ids": valid_course_id_list, "program_ids": valid_program_id_list, + "slug": department.slug, }, ) diff --git a/frontend/public/src/containers/App.js b/frontend/public/src/containers/App.js index e5d719b0b..1b5496df6 100644 --- a/frontend/public/src/containers/App.js +++ b/frontend/public/src/containers/App.js @@ -115,6 +115,14 @@ export class App extends React.Component { path={urljoin(match.url, String(routes.learnerRecords))} component={LearnerRecordsPage} /> + + + departments: ?Array, + match: Match } // Department filter name for all items. @@ -55,6 +57,8 @@ const PROGRAMS_TAB = "programs" // Course tab name. const COURSES_TAB = "courses" +const TABS = [PROGRAMS_TAB, COURSES_TAB] + export class CatalogPage extends React.Component { state = { tabSelected: COURSES_TAB, @@ -81,6 +85,20 @@ export class CatalogPage extends React.Component { super(props) this.io = null this.container = React.createRef(null) + const { match } = this.props + if (match) { + const { tab, department } = match.params + if (TABS.includes(tab)) { + this.state.tabSelected = tab + } else { + this.state.tabSelected = COURSES_TAB + } + if (department) { + this.state.selectedDepartment = department + } else { + this.state.selectedDepartment = ALL_DEPARTMENTS + } + } } componentWillUnmount() { @@ -130,7 +148,7 @@ export class CatalogPage extends React.Component { this.state.allCoursesRetrieved, response.body.results ) - const filteredCourses = this.filteredCoursesBasedOnCourseRunCriteria( + const filteredCourses = this.filteredCoursesBasedOnSelectedDepartment( this.state.selectedDepartment, allCourses ) @@ -216,7 +234,7 @@ export class CatalogPage extends React.Component { } if (!coursesIsLoading && !this.state.filterCoursesCalled) { this.setState({ filterCoursesCalled: true }) - const filteredCourses = this.filteredCoursesBasedOnCourseRunCriteria( + const filteredCourses = this.filteredCoursesBasedOnSelectedDepartment( this.state.selectedDepartment, this.state.allCoursesRetrieved ) @@ -247,28 +265,29 @@ export class CatalogPage extends React.Component { } /** - * Returns an array of departments names which have one or more course(s) or program(s) + * Returns an array of departments objects 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. */ filterDepartmentsByTabName(selectedTabName: string) { if (!this.props.departmentsIsLoading) { const { departments } = this.props + const allDepartments = { name: ALL_DEPARTMENTS, slug: ALL_DEPARTMENTS } if (selectedTabName === COURSES_TAB) { return [ ...new Set([ - ALL_DEPARTMENTS, + allDepartments, ...departments.flatMap(department => - department.course_ids.length > 0 ? department.name : [] + department.course_ids.length > 0 ? department : [] ) ]) ] } else { return [ ...new Set([ - ALL_DEPARTMENTS, + allDepartments, ...departments.flatMap(department => - department.program_ids.length > 0 ? department.name : [] + department.program_ids.length > 0 ? department : [] ) ]) ] @@ -340,7 +359,7 @@ export class CatalogPage extends React.Component { } else { coursesToFilter.push(...this.state.allCoursesRetrieved) } - const filteredCourses = this.filteredCoursesBasedOnCourseRunCriteria( + const filteredCourses = this.filteredCoursesBasedOnSelectedDepartment( this.state.selectedDepartment, coursesToFilter ) @@ -376,7 +395,7 @@ export class CatalogPage extends React.Component { changeSelectedDepartment = (selectedDepartment: string) => { this.resetQueryVariablesToDefault() this.setState({ selectedDepartment: selectedDepartment }) - const filteredCourses = this.filteredCoursesBasedOnCourseRunCriteria( + const filteredCourses = this.filteredCoursesBasedOnSelectedDepartment( selectedDepartment, this.state.allCoursesRetrieved ) @@ -407,8 +426,12 @@ export class CatalogPage extends React.Component { departments.length > 0 ) { const newDepartment = this.props.departments.find( - department => department.name === selectedDepartment + department => department.slug === selectedDepartment ) + if (!newDepartment) { + this.setState({ selectedDepartment: ALL_DEPARTMENTS }) + return + } if ( filteredCourses.length !== newDepartment.course_ids.length && !this.state.isLoadingMoreItems @@ -428,7 +451,7 @@ export class CatalogPage extends React.Component { this.setState({ allCoursesRetrieved: allCourses }) this.setState({ courseQueryPage: 2 }) this.setState({ queryIDListString: remainingIDs.toString() }) - const filteredCourses = this.filteredCoursesBasedOnCourseRunCriteria( + const filteredCourses = this.filteredCoursesBasedOnSelectedDepartment( selectedDepartment, allCourses ) @@ -497,29 +520,34 @@ export class CatalogPage extends React.Component { /** * Returns a filtered array of courses which have: an associated Department name matching the selectedDepartment * if the selectedDepartment does not equal "All Departments", - * an associated page which is live, and at least 1 associated Course Run. + * This function, at one time, checked for an associated page which is live, and at least 1 associated Course Run. + * This logic was removed as this is handled by the API & would cause coursecount and programcount to be incorrect. * @param {Array} courses An array of courses which will be filtered by Department. * @param {string} selectedDepartment The Department name used to compare against the courses in the array. */ - filteredCoursesBasedOnCourseRunCriteria( + filteredCoursesBasedOnSelectedDepartment( selectedDepartment: string, courses: Array ) { - return courses.filter( - course => - (selectedDepartment === ALL_DEPARTMENTS || - course.departments - .map(department => department.name) - .includes(selectedDepartment)) && - course?.page?.live && - course.courseruns.length > 0 && - this.validateCoursesCourseRuns(course.courseruns).length > 0 - ) + const { departments } = this.props + if (this.state.selectedDepartment === ALL_DEPARTMENTS) { + return courses + } else { + const selectedDepartment = departments.find( + department => department.slug === this.state.selectedDepartment + ) + if (!selectedDepartment) { + this.setState({ selectedDepartment: ALL_DEPARTMENTS }) + return courses + } + return courses.filter(course => + selectedDepartment.course_ids.includes(course.id) + ) + } } /** - * Returns an array of Programs which have page.live = true and a department name which - * matches the currently selected department. + * Returns an array of Programs which relate to the selectedDepartment using the department's list of IDs. * @param {Array} programs An array of Programs which will be filtered by Department and other criteria. * @param {string} selectedDepartment The Department name used to compare against the courses in the array. */ @@ -527,38 +555,66 @@ export class CatalogPage extends React.Component { selectedDepartment: string, programs: Array ) { - return programs.filter( - program => - selectedDepartment === ALL_DEPARTMENTS || + const { departments } = this.props + if (this.state.selectedDepartment === ALL_DEPARTMENTS) { + return programs + } else { + const selectedDepartment = departments.find( + department => department.slug === this.state.selectedDepartment + ) + if (!selectedDepartment) { + this.setState({ selectedDepartment: ALL_DEPARTMENTS }) + return programs + } + return programs.filter(program => program.departments .map(department => department) - .includes(selectedDepartment) - ) + .includes(selectedDepartment.name) + ) + } } /** - * Returns the number of courseRuns or programs based on the selected catalog tab. + * Returns the number of courses based on the selectedDepartment. + * If the selectedDepartment is "All Departments", the total number of courses is returned. + * If the selectedDepartment is not found in the departments array, 0 is returned. + * @returns {number} */ renderNumberOfCatalogCourses() { const { departments } = this.props - if (this.state.selectedDepartment === ALL_DEPARTMENTS) { + const selectedDepartment = this.state.selectedDepartment + if (selectedDepartment === ALL_DEPARTMENTS) { return this.state.allCoursesCount - } else if (this.state.selectedDepartment !== ALL_DEPARTMENTS) { + } else if (!departments) return 0 + const departmentSlugs = departments.map(department => department.slug) + if (!departmentSlugs.includes(selectedDepartment)) { + return 0 + } else { return departments.find( - department => department.name === this.state.selectedDepartment + department => department.slug === this.state.selectedDepartment ).course_ids.length } } + /** Returns the number of programs based on the selectedDepartment + * or all programs if the selectedDepartment is "All Departments". + * If the selectedDepartment is not found in the departments array, 0 is returned. + * @returns {number} + */ renderNumberOfCatalogPrograms() { const { departments } = this.props + if (!departments) return 0 + const departmentSlugs = departments.map(department => department.slug) + const selectedDepartment = this.state.selectedDepartment if (this.state.selectedDepartment === ALL_DEPARTMENTS) { return this.state.allProgramsCount - } else if (this.state.selectedDepartment !== ALL_DEPARTMENTS) { + } else if (!departmentSlugs.includes(selectedDepartment)) { + return 0 + } else { return departments.find( - department => department.name === this.state.selectedDepartment + department => department.slug === this.state.selectedDepartment ).program_ids.length - } else return 0 + } } /** @@ -682,18 +738,21 @@ export class CatalogPage extends React.Component { departmentSideBarListItems.push(
  • ) diff --git a/frontend/public/src/containers/pages/CatalogPage_test.js b/frontend/public/src/containers/pages/CatalogPage_test.js index 9c4532113..074d64a4a 100644 --- a/frontend/public/src/containers/pages/CatalogPage_test.js +++ b/frontend/public/src/containers/pages/CatalogPage_test.js @@ -170,13 +170,31 @@ describe("CatalogPage", function() { courses: { isPending: false, status: 200 + }, + departments: { + isPending: false, + status: 200 } }, entities: { courses: { count: 1, results: courses - } + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1], + program_ids: [1] + }, + { + name: "Science", + slug: "science", + course_ids: [1], + program_ids: [1] + } + ] } }, {} @@ -226,11 +244,13 @@ describe("CatalogPage", function() { departments: [ { name: "History", + slug: "history", course_ids: [], program_ids: [1, 2, 3, 4, 5] }, { name: "Science", + slug: "science", course_ids: [2], program_ids: [] } @@ -253,6 +273,34 @@ describe("CatalogPage", function() { }) it("renders catalog department filter for courses and programs tabs", async () => { + const allDepartments = { + name: "All Departments", + slug: "All Departments" + } + const department1 = { + name: "department1", + slug: "department1", + course_ids: [1], + program_ids: [] + } + const department2 = { + name: "department2", + slug: "department2", + course_ids: [1], + program_ids: [1] + } + const department3 = { + name: "department3", + slug: "department3", + course_ids: [], + program_ids: [1] + } + const department4 = { + name: "department4", + slug: "department4", + course_ids: [], + program_ids: [] + } const { inner } = await renderPage( { queries: { @@ -262,28 +310,7 @@ describe("CatalogPage", function() { } }, entities: { - departments: [ - { - name: "department1", - course_ids: [1], - program_ids: [] - }, - { - name: "department2", - course_ids: [1], - program_ids: [1] - }, - { - name: "department3", - course_ids: [], - program_ids: [1] - }, - { - name: "department4", - course_ids: [], - program_ids: [] - } - ] + departments: [department1, department2, department3, department4] } }, {} @@ -292,69 +319,93 @@ describe("CatalogPage", function() { .instance() .filterDepartmentsByTabName("courses") expect(JSON.stringify(filteredDepartments)).equals( - JSON.stringify(["All Departments", "department1", "department2"]) + JSON.stringify([allDepartments, department1, department2]) ) filteredDepartments = inner .instance() .filterDepartmentsByTabName("programs") expect(JSON.stringify(filteredDepartments)).equals( - JSON.stringify(["All Departments", "department2", "department3"]) + JSON.stringify([allDepartments, department2, department3]) ) }) it("renders catalog courses when filtered by department", async () => { const course1 = JSON.parse(JSON.stringify(displayedCourse)) course1.departments = [{ name: "Math" }] + course1.id = 1 const course2 = JSON.parse(JSON.stringify(displayedCourse)) course2.departments = [{ name: "Science" }] + course2.id = 2 const course3 = JSON.parse(JSON.stringify(displayedCourse)) course3.departments = [{ name: "Science" }] + course3.id = 3 courses = [course1, course2, course3] - const { inner } = await renderPage() + const { inner } = await renderPage( + { + queries: { + courses: { + isPending: false, + status: 200 + }, + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + courses: { + count: 3, + results: courses + }, + programs: { + results: [displayedProgram] + }, + departments: [ + { + name: "Science", + slug: "science", + course_ids: [1, 3], + program_ids: [2, 3] + }, + { + name: "Math", + slug: "math", + course_ids: [1], + program_ids: [1] + }, + { + name: "department4", + slug: "department4", + course_ids: [], + program_ids: [] + } + ] + } + }, + {} + ) + inner.state().tabSelected = "courses" + inner.state().selectedDepartment = "math" let coursesFilteredByCriteriaAndDepartment = inner .instance() - .filteredCoursesBasedOnCourseRunCriteria("Math", courses) + .filteredCoursesBasedOnSelectedDepartment("math", courses) expect(coursesFilteredByCriteriaAndDepartment.length).equals(1) + inner.state().selectedDepartment = "science" coursesFilteredByCriteriaAndDepartment = inner .instance() - .filteredCoursesBasedOnCourseRunCriteria("Science", courses) + .filteredCoursesBasedOnSelectedDepartment("science", courses) expect(coursesFilteredByCriteriaAndDepartment.length).equals(2) + inner.state().selectedDepartment = "All Departments" coursesFilteredByCriteriaAndDepartment = inner .instance() - .filteredCoursesBasedOnCourseRunCriteria("All Departments", courses) + .filteredCoursesBasedOnSelectedDepartment("All Departments", courses) expect(coursesFilteredByCriteriaAndDepartment.length).equals(3) }) - it("renders no catalog courses if the course's pages are not live", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - course.page.live = false - const { inner } = await renderPage() - const coursesFilteredByCriteriaAndDepartment = inner - .instance() - .filteredCoursesBasedOnCourseRunCriteria("All Departments", [course]) - expect(coursesFilteredByCriteriaAndDepartment.length).equals(0) - }) - - it("renders no catalog courses if the course has no page", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - delete course.page - const { inner } = await renderPage() - const coursesFilteredByCriteriaAndDepartment = inner - .instance() - .filteredCoursesBasedOnCourseRunCriteria("All Departments", [course]) - expect(coursesFilteredByCriteriaAndDepartment.length).equals(0) - }) - - it("renders no catalog courses if the course has no associated course runs", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - course.courseruns = [] - const { inner } = await renderPage() - const coursesFilteredByCriteriaAndDepartment = inner - .instance() - .filteredCoursesBasedOnCourseRunCriteria("All Departments", [course]) - expect(coursesFilteredByCriteriaAndDepartment.length).equals(0) - }) - it("renders catalog programs when filtered by department", async () => { const program1 = JSON.parse(JSON.stringify(displayedProgram)) program1.departments = ["Math"] @@ -362,16 +413,57 @@ describe("CatalogPage", function() { program2.departments = ["History"] const program3 = JSON.parse(JSON.stringify(displayedProgram)) program3.departments = ["History"] - const { inner } = await renderPage() programs = [program1, program2, program3] + const { inner } = await renderPage({ + queries: { + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + programs: { + count: 3, + results: [program1, program2, program3] + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1, 2], + program_ids: [2, 3] + }, + { + name: "Math", + slug: "math", + course_ids: [1, 2, 3], + program_ids: [1] + }, + { + name: "department4", + slug: "department4", + course_ids: [], + program_ids: [] + } + ] + } + }) + inner.state().tabSelected = "programs" + inner.state().selectedDepartment = "math" let programsFilteredByCriteriaAndDepartment = inner .instance() - .filteredProgramsByDepartmentAndCriteria("Math", programs) + .filteredProgramsByDepartmentAndCriteria("math", programs) expect(programsFilteredByCriteriaAndDepartment.length).equals(1) + inner.state().selectedDepartment = "history" programsFilteredByCriteriaAndDepartment = inner .instance() - .filteredProgramsByDepartmentAndCriteria("History", programs) + .filteredProgramsByDepartmentAndCriteria("history", programs) expect(programsFilteredByCriteriaAndDepartment.length).equals(2) + inner.state().selectedDepartment = "All Departments" programsFilteredByCriteriaAndDepartment = inner .instance() .filteredProgramsByDepartmentAndCriteria("All Departments", programs) @@ -493,10 +585,13 @@ describe("CatalogPage", function() { it("renders catalog courses based on selected department", async () => { const course1 = JSON.parse(JSON.stringify(displayedCourse)) course1.departments = [{ name: "Math" }] + course1.id = 1 const course2 = JSON.parse(JSON.stringify(displayedCourse)) course2.departments = [{ name: "Math" }, { name: "History" }] + course2.id = 2 const course3 = JSON.parse(JSON.stringify(displayedCourse)) course3.departments = [{ name: "Math" }, { name: "History" }] + course3.id = 3 courses = [course1, course2, course3] const { inner } = await renderPage( { @@ -525,16 +620,19 @@ describe("CatalogPage", function() { departments: [ { name: "History", - course_ids: [1, 2], + slug: "history", + course_ids: [2, 3], program_ids: [1] }, { name: "Math", + slug: "math", course_ids: [1, 2, 3], program_ids: [] }, { name: "department4", + slug: "department4", course_ids: [], program_ids: [] } @@ -559,9 +657,10 @@ describe("CatalogPage", function() { expect(inner.instance().renderNumberOfCatalogCourses()).equals(3) // Select a department to filter by. - inner.instance().changeSelectedDepartment("History", "courses") + inner.instance().changeSelectedDepartment("history") // Confirm the state updated to reflect the selected department. - expect(inner.state().selectedDepartment).equals("History") + expect(inner.state().selectedDepartment).equals("history") + expect(inner.state().tabSelected).equals("courses") // Confirm the number of catalog items updated to reflect the items filtered by department. expect(inner.instance().renderNumberOfCatalogCourses()).equals(2) // Confirm the courses filtered are those which have a department name matching the selected department. @@ -575,7 +674,7 @@ describe("CatalogPage", function() { // Change to the programs tab. inner.instance().changeSelectedTab("programs") // Confirm that the selected department is the same as before. - expect(inner.state().selectedDepartment).equals("History") + expect(inner.state().selectedDepartment).equals("history") // Change back to the courses tab. inner.instance().changeSelectedTab("courses") @@ -884,7 +983,47 @@ describe("CatalogPage", function() { }) it("renderCatalogCount is plural for more than one course", async () => { - const { inner } = await renderPage() + const displayedProgram2 = JSON.parse(JSON.stringify(displayedProgram)) + displayedProgram2.id = 2 + const displayedCourse2 = JSON.parse(JSON.stringify(displayedCourse)) + displayedCourse2.id = 2 + const { inner } = await renderPage( + { + queries: { + courses: { + isPending: false, + status: 200 + }, + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + courses: { + count: 2, + results: [displayedCourse, displayedCourse2] + }, + programs: { + count: 2, + results: [displayedProgram, displayedProgram2] + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1], + program_ids: [1] + } + ] + } + }, + {} + ) inner.setState({ tabSelected: "courses" }) inner.setState({ selectedDepartment: "All Departments" }) inner.setState({ allCoursesCount: 2 }) @@ -892,7 +1031,47 @@ describe("CatalogPage", function() { }) it("renderCatalogCount is plural for more than one program", async () => { - const { inner } = await renderPage() + const displayedProgram2 = JSON.parse(JSON.stringify(displayedProgram)) + displayedProgram2.id = 2 + const displayedCourse2 = JSON.parse(JSON.stringify(displayedCourse)) + displayedCourse2.id = 2 + const { inner } = await renderPage( + { + queries: { + courses: { + isPending: false, + status: 200 + }, + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + courses: { + count: 2, + results: [displayedCourse, displayedCourse2] + }, + programs: { + count: 2, + results: [displayedProgram, displayedProgram2] + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1], + program_ids: [1] + } + ] + } + }, + {} + ) inner.setState({ tabSelected: "programs" }) inner.setState({ selectedDepartment: "All Departments" }) inner.setState({ allProgramsCount: 2 }) @@ -900,7 +1079,43 @@ describe("CatalogPage", function() { }) it("renderCatalogCount is singular for one course", async () => { - const { inner } = await renderPage() + const { inner } = await renderPage( + { + queries: { + courses: { + isPending: false, + status: 200 + }, + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + courses: { + count: 1, + results: [displayedCourse] + }, + programs: { + count: 1, + results: [displayedProgram] + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1], + program_ids: [1] + } + ] + } + }, + {} + ) inner.setState({ tabSelected: "courses" }) inner.setState({ selectedDepartment: "All Departments" }) inner.setState({ allCoursesCount: 1 }) @@ -908,10 +1123,47 @@ describe("CatalogPage", function() { }) it("renderCatalogCount is singular for one program", async () => { - const { inner } = await renderPage() + const { inner } = await renderPage( + { + queries: { + courses: { + isPending: false, + status: 200 + }, + programs: { + isPending: false, + status: 200 + }, + departments: { + isPending: false, + status: 200 + } + }, + entities: { + courses: { + count: 1, + results: [displayedCourse] + }, + programs: { + count: 1, + results: [displayedProgram] + }, + departments: [ + { + name: "History", + slug: "history", + course_ids: [1], + program_ids: [1] + } + ] + } + }, + {} + ) inner.setState({ tabSelected: "programs" }) inner.setState({ selectedDepartment: "All Departments" }) inner.setState({ allProgramsCount: 1 }) + inner.instance().componentDidUpdate({}, {}) expect(inner.find("h2.catalog-count").text()).equals("1 program") }) }) diff --git a/frontend/public/src/lib/urls.js b/frontend/public/src/lib/urls.js index 92d8ea0d2..b8fff193b 100644 --- a/frontend/public/src/lib/urls.js +++ b/frontend/public/src/lib/urls.js @@ -5,13 +5,15 @@ import qs from "query-string" export const getNextParam = (search: string) => qs.parse(search).next || "/" export const routes = { - root: "/", - dashboard: "/dashboard/", - profile: "/profile/", - accountSettings: "/account-settings/", - logout: "/logout/", - orderHistory: "/orders/history", - catalog: "/catalog", + root: "/", + dashboard: "/dashboard/", + profile: "/profile/", + accountSettings: "/account-settings/", + logout: "/logout/", + orderHistory: "/orders/history", + catalogTabByDepartment: "/catalog/:tab/:department", + catalogTab: "/catalog/:tab", + catalog: "/catalog/", // authentication related routes login: include("/signin/", {