diff --git a/frontend/public/src/components/CourseInfoBox.js b/frontend/public/src/components/CourseInfoBox.js index 834484c07d..c6ba0aeb8c 100644 --- a/frontend/public/src/components/CourseInfoBox.js +++ b/frontend/public/src/components/CourseInfoBox.js @@ -3,7 +3,8 @@ import { formatPrettyDate, emptyOrNil, getFlexiblePriceForProduct, - formatLocalePrice + formatLocalePrice, + getStartDateText } from "../lib/util" import moment from "moment-timezone" @@ -20,16 +21,6 @@ type CourseInfoBoxProps = { setCurrentCourseRun: (run: EnrollmentFlaggedCourseRun) => Promise } -const getStartDateText = (run: BaseCourseRun, isArchived: boolean = false) => { - if (isArchived) { - return "Course content available anytime" - } - return run && !emptyOrNil(run.start_date) && !run.is_self_paced - ? (run.start_date > moment() ? "Starts: " : "Started: ") + - formatPrettyDate(moment(new Date(run.start_date))) - : "Start Anytime" -} - export default class CourseInfoBox extends React.PureComponent { state = { showMoreEnrollDates: false @@ -84,7 +75,7 @@ export default class CourseInfoBox extends React.PureComponent ) } @@ -110,7 +101,9 @@ export default class CourseInfoBox extends React.PureComponent
- {getStartDateText(run, isArchived)} + {isArchived + ? "Course content available anytime" + : getStartDateText(run)}
{!isArchived && moreEnrollableCourseRuns ? ( <> diff --git a/frontend/public/src/containers/pages/CatalogPage.js b/frontend/public/src/containers/pages/CatalogPage.js index 971c2c4b07..b2cba9f1b3 100644 --- a/frontend/public/src/containers/pages/CatalogPage.js +++ b/frontend/public/src/containers/pages/CatalogPage.js @@ -1,7 +1,7 @@ import React from "react" import { CSSTransition, TransitionGroup } from "react-transition-group" import moment from "moment" -import { parseDateString, formatPrettyDate } from "../../lib/util" +import { getStartDateText } from "../../lib/util" import { coursesSelector, @@ -298,58 +298,6 @@ export class CatalogPage extends React.Component { this.toggleMobileFilterWindowExpanded(false) } - /** - * This is a comparison method used to sort an array of Course Runs - * from earliest start date to latest start date. - * @param {BaseCourseRun} courseRunA The first Course Run to compare. - * @param {BaseCourseRun} courseRunB The second Course Run to compare. - */ - compareCourseRunStartDates( - courseRunA: BaseCourseRun, - courseRunB: BaseCourseRun - ) { - if (moment(courseRunA.start_date).isBefore(courseRunB.start_date)) { - return -1 - } - if (moment(courseRunA.start_date).isAfter(courseRunB.start_date)) { - return 1 - } - // CourseRunA and CourseRunB share the same start date. - return 0 - } - - /** - * Returns the text to be displayed on a course catalog card's tag. - * This text will either be "Start Anytime" or "Start Date: ". - * If the Course has at least one associated Course Run which is not self-paced, and - * Course Run start date is in the future, then return "Start Date: ". - * If the Course has at least one associated Course Run which is not self-paced, and - * Course Run start date is in the past, then return "Start Anytime". - * If the course only has Course Runs which are self-paced, display "Start Anytime". - * @param {CourseDetailWithRuns} course The course being evaluated. - */ - renderCatalogCardTagForCourse(course: CourseDetailWithRuns) { - const nonSelfPacedCourseRuns = course.courseruns.filter( - courseRun => !courseRun.is_self_paced - ) - if (nonSelfPacedCourseRuns.length > 0) { - const futureStartDateCourseRuns = nonSelfPacedCourseRuns.filter( - courseRun => moment(courseRun.start_date).isAfter(moment()) - ) - if (futureStartDateCourseRuns.length > 0) { - const startDate = parseDateString( - futureStartDateCourseRuns.sort(this.compareCourseRunStartDates)[0] - .start_date - ) - return `Start Date: ${formatPrettyDate(startDate)}` - } else { - return "Start Anytime" - } - } else { - return "Start Anytime" - } - } - /** * 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 @@ -436,7 +384,7 @@ export class CatalogPage extends React.Component { />
- {this.renderCatalogCardTagForCourse(course)} + {getStartDateText(course)}
{course.title}
diff --git a/frontend/public/src/containers/pages/CatalogPage_test.js b/frontend/public/src/containers/pages/CatalogPage_test.js index b64ef111b1..9caf2b59f2 100644 --- a/frontend/public/src/containers/pages/CatalogPage_test.js +++ b/frontend/public/src/containers/pages/CatalogPage_test.js @@ -4,7 +4,6 @@ import { expect } from "chai" import moment from "moment-timezone" import IntegrationTestHelper from "../../util/integration_test_helper" import CatalogPage, { CatalogPage as InnerCatalogPage } from "./CatalogPage" -import { formatPrettyDate } from "../../lib/util" import sinon from "sinon" const displayedCourse = { @@ -486,86 +485,6 @@ describe("CatalogPage", function() { expect(coursesFilteredByCriteriaAndDepartment.length).equals(1) }) - it("renders catalog course card with Start Anytime label if non-self paced course runs exist and all course runs start date in the past", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - const { inner } = await renderPage() - let catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals("Start Anytime") - course.courseruns[0].start_date = moment().add(2, "M") - catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals( - `Start Date: ${formatPrettyDate(course.courseruns[0].start_date)}` - ) - }) - - it("renders catalog course card with start date label if non-self paced course runs exist and all course runs start in the future", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - const { inner } = await renderPage() - let catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals("Start Anytime") - course.courseruns[0].start_date = moment().add(2, "M") - catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals( - `Start Date: ${formatPrettyDate(course.courseruns[0].start_date)}` - ) - }) - - it("renders catalog course card with closest future start date label if non-self paced course runs exist and a course run start date is in the future", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - // Associate a second course run with the course - course.courseruns.push( - JSON.parse(JSON.stringify(displayedCourse.courseruns[0])) - ) - course.courseruns.push( - JSON.parse(JSON.stringify(displayedCourse.courseruns[0])) - ) - course.courseruns.push( - JSON.parse(JSON.stringify(displayedCourse.courseruns[0])) - ) - const { inner } = await renderPage() - let catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals("Start Anytime") - - // Update the start dates of each associated course run to be in the future. - course.courseruns[0].start_date = moment().add(2, "M") - course.courseruns[1].start_date = moment().add(3, "d") - course.courseruns[2].start_date = moment().add(2, "d") - course.courseruns[3].is_self_paced = true - catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - // Expect the closest future start date - expect(catalogCardTagForCourse).equals( - `Start Date: ${formatPrettyDate(course.courseruns[2].start_date)}` - ) - }) - - it("renders catalog course card with Start Anytime if only self paced course runs exist, even if start date is in the future", async () => { - const course = JSON.parse(JSON.stringify(displayedCourse)) - const { inner } = await renderPage() - let catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals("Start Anytime") - - course.courseruns[0].start_date = moment().add(2, "M") - course.courseruns[0].is_self_paced = true - catalogCardTagForCourse = inner - .instance() - .renderCatalogCardTagForCourse(course) - expect(catalogCardTagForCourse).equals("Start Anytime") - }) - it("renders catalog courses based on selected department", async () => { const course1 = JSON.parse(JSON.stringify(displayedCourse)) course1.departments = [{ name: "Math" }] diff --git a/frontend/public/src/lib/util.js b/frontend/public/src/lib/util.js index 59df80430d..243a5b6ee2 100644 --- a/frontend/public/src/lib/util.js +++ b/frontend/public/src/lib/util.js @@ -23,6 +23,7 @@ import posthog from "posthog-js" import type Moment from "moment" import type { HttpRespErrorMessage, HttpResponse } from "../flow/httpTypes" import type { Product } from "../flow/cartTypes" +import type { BaseCourseRun, CourseDetailWithRuns } from "../flow/courseTypes" import { DISCOUNT_TYPE_DOLLARS_OFF, @@ -283,3 +284,92 @@ export const intCheckFeatureFlag = ( export const checkFeatureFlag = (flag: string, uniqueID: string | number) => { return intCheckFeatureFlag(flag, uniqueID, document, SETTINGS) } + +/** + * This is a comparison method used to sort an array of Course Runs + * from earliest start date to latest start date. + * @param {BaseCourseRun} courseRunA The first Course Run to compare. + * @param {BaseCourseRun} courseRunB The second Course Run to compare. + */ +export const compareCourseRunStartDates = ( + courseRunA: BaseCourseRun, + courseRunB: BaseCourseRun +) => { + if (moment(courseRunA.start_date).isBefore(courseRunB.start_date)) { + return -1 + } + if (moment(courseRunA.start_date).isAfter(courseRunB.start_date)) { + return 1 + } + // CourseRunA and CourseRunB share the same start date. + return 0 +} + +/** + * This is a comparison method used to sort an array of Course Runs + * from latest start date to earliest start date. + * @param {BaseCourseRun} courseRunA The first Course Run to compare. + * @param {BaseCourseRun} courseRunB The second Course Run to compare. + */ +export const reverseCompareCourseRunStartDates = ( + courseRunA: BaseCourseRun, + courseRunB: BaseCourseRun +) => { + if (moment(courseRunA.start_date).isBefore(courseRunB.start_date)) { + return 1 + } + if (moment(courseRunA.start_date).isAfter(courseRunB.start_date)) { + return -1 + } + // CourseRunA and CourseRunB share the same start date. + return 0 +} + +/** + * Returns the text to be displayed on a course catalog card's tag. + * This text will either be "Start Anytime" or "Start Date: ". + * If the Course has at least one associated Course Run which is not self-paced, and + * Course Run start date is in the future, then return "Start Date: ". + * If the Course has at least one associated Course Run which is not self-paced, and + * Course Run start date is in the past, and showPast is not true, then return "Start Anytime". + * If the Course has at least one associated Course Run which is not self-paced, and + * Course Run start date is in the past, and showPast is true, then return "Start Date: ". + * If the course only has Course Runs which are self-paced, display "Start Anytime". + * @param {CourseDetailWithRuns|BaseCourseRun} course The course being evaluated, or an individual course run to display the start text for. + * @param {showPast} boolean If the start date for the course is in the past, and showPast is true, then render the most recent start date for the course. + */ + +export const getStartDateText = ( + courseware: BaseCourseRun | CourseDetailWithRuns, + showPast: boolean = false +) => { + const nonSelfPacedCourseRuns = courseware.courseruns + ? courseware.courseruns.filter(courseRun => !courseRun.is_self_paced) + : courseware.is_self_paced + ? [] + : [courseware] + + if (nonSelfPacedCourseRuns.length > 0) { + const futureStartDateCourseRuns = nonSelfPacedCourseRuns.filter(courseRun => + moment(courseRun.start_date).isAfter(moment()) + ) + if (futureStartDateCourseRuns.length > 0) { + const startDate = parseDateString( + futureStartDateCourseRuns.sort(compareCourseRunStartDates)[0].start_date + ) + return `Start Date: ${formatPrettyDate(startDate)}` + } else { + if (showPast) { + return `Start Date: ${formatPrettyDate( + parseDateString( + nonSelfPacedCourseRuns.sort(reverseCompareCourseRunStartDates)[0] + .start_date + ) + )}` + } + return "Start Anytime" + } + } else { + return "Start Anytime" + } +} diff --git a/frontend/public/src/lib/util_test.js b/frontend/public/src/lib/util_test.js index ab348f8223..f05e6295d6 100644 --- a/frontend/public/src/lib/util_test.js +++ b/frontend/public/src/lib/util_test.js @@ -26,7 +26,8 @@ import { isErrorResponse, isUnauthorizedResponse, formatLocalePrice, - intCheckFeatureFlag + intCheckFeatureFlag, + getStartDateText } from "./util" describe("utility functions", () => { @@ -335,4 +336,103 @@ describe("utility functions", () => { ) }) }) + + describe("getStartDateText", () => { + [ + ["course", "past", false, "Start Anytime"], + ["course run", "past", false, "Start Anytime"], + ["course", "future", false, "Start Date"], + ["course run", "future", false, "Start Date"], + ["course", "past", true, "Start Anytime"], + ["course run", "past", true, "Start Anytime"], + ["course", "future", true, "Start Anytime"], + ["course run", "future", true, "Start Anytime"] + ].forEach(([coursewareType, datePosition, selfPaced, displayText]) => { + it(`displays the ${displayText} text when the ${coursewareType} is in the ${datePosition} and there ${ + selfPaced ? "are" : "are no" + } self-paced courses`, () => { + const course = { + courseruns: [ + { + start_date: + datePosition === "future" + ? moment().add(1, "days") + : moment().subtract(1, "days"), + is_self_paced: false + } + ] + } + + if (selfPaced) { + course["courseruns"][0]["is_self_paced"] = true + } + + assert.isTrue( + getStartDateText( + coursewareType === "course" ? course : course["courseruns"][0] + ).includes(displayText) + ) + }) + }) + + it("displays the closest start date if there are multiple future start dates", () => { + const startDates = [ + moment().add(1, "days"), + moment().add(2, "days"), + moment().add(3, "days") + ] + + const course = { + courseruns: [ + { + start_date: startDates[0] + }, + { + start_date: startDates[1] + }, + { + start_date: startDates[2] + } + ] + } + + assert.isTrue(getStartDateText(course).includes("Start Date")) + assert.isTrue( + getStartDateText(course).includes(formatPrettyDate(startDates[0])) + ) + assert.isFalse( + getStartDateText(course).includes(formatPrettyDate(startDates[2])) + ) + }) + + it("displays the closest start date if there are multiple past start dates and showPast is true", () => { + const startDates = [ + moment().subtract(1, "days"), + moment().subtract(2, "days"), + moment().subtract(3, "days") + ] + + const course = { + courseruns: [ + { + start_date: startDates[2] + }, + { + start_date: startDates[1] + }, + { + start_date: startDates[0] + } + ] + } + + assert.isTrue(getStartDateText(course, true).includes("Start Date")) + assert.isTrue( + getStartDateText(course, true).includes(formatPrettyDate(startDates[0])) + ) + assert.isFalse( + getStartDateText(course, true).includes(formatPrettyDate(startDates[2])) + ) + }) + }) })