Skip to content

Commit

Permalink
fix: ensure course about page acknowledges run-based assignments (#1181)
Browse files Browse the repository at this point in the history
  • Loading branch information
adamstankiewicz authored Sep 12, 2024
1 parent bd4087e commit 0d978d2
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 107 deletions.
7 changes: 2 additions & 5 deletions src/components/app/data/hooks/useCourseMetadata.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { useParams, useSearchParams } from 'react-router-dom';

import { queryCourseMetadata } from '../queries';
import {
determineAllocatedCourseRunAssignmentsForCourse,
determineAllocatedAssignmentsForCourse,
getAvailableCourseRuns,
transformCourseMetadataByAllocatedCourseRunAssignments,
} from '../utils';
Expand All @@ -23,10 +23,7 @@ export default function useCourseMetadata(queryOptions = {}) {
allocatedCourseRunAssignmentKeys,
hasAssignedCourseRuns,
hasMultipleAssignedCourseRuns,
} = determineAllocatedCourseRunAssignmentsForCourse({
courseKey,
redeemableLearnerCreditPolicies,
});
} = determineAllocatedAssignmentsForCourse({ courseKey, redeemableLearnerCreditPolicies });
// `requestUrl.searchParams` uses `URLSearchParams`, which decodes `+` as a space, so we
// need to replace it with `+` again to be a valid course run key.
let courseRunKey = searchParams.get('course_run_key')?.replaceAll(' ', '+');
Expand Down
26 changes: 22 additions & 4 deletions src/components/app/data/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -806,27 +806,45 @@ export function isEnrollmentUpgradeable(enrollment) {
* }
* }
*/
export function determineAllocatedCourseRunAssignmentsForCourse({
export function determineAllocatedAssignmentsForCourse({
redeemableLearnerCreditPolicies,
courseKey,
}) {
const { learnerContentAssignments } = redeemableLearnerCreditPolicies;
// note: checking the non-happy path first, with early return so happy path code isn't nested in conditional.
if (!learnerContentAssignments.hasAllocatedAssignments) {
return {
isCourseAssigned: false,
allocatedAssignmentsForCourse: [],
allocatedCourseRunAssignmentKeys: [],
allocatedCourseRunAssignments: [],
hasAssignedCourseRuns: false,
hasMultipleAssignedCourseRuns: false,
};
}
const allocatedCourseRunAssignments = learnerContentAssignments.allocatedAssignments.filter((assignment) => (
assignment.isAssignedCourseRun && assignment.parentContentKey === courseKey
));

const allocatedAssignmentsForCourse = [];
const allocatedCourseRunAssignments = [];

learnerContentAssignments.allocatedAssignments.forEach((assignment) => {
const isCourseRunAssignment = assignment.isAssignedCourseRun && assignment.parentContentKey === courseKey;
const isCourseAssignment = !assignment.isAssignedCourseRun && assignment.contentKey === courseKey;
if (isCourseRunAssignment || isCourseAssignment) {
allocatedAssignmentsForCourse.push(assignment);
}
if (isCourseRunAssignment) {
allocatedCourseRunAssignments.push(assignment);
}
});

const isCourseAssigned = allocatedAssignmentsForCourse.length > 0;
const allocatedCourseRunAssignmentKeys = allocatedCourseRunAssignments.map(assignment => assignment.contentKey);
const hasAssignedCourseRuns = allocatedCourseRunAssignmentKeys.length > 0;
const hasMultipleAssignedCourseRuns = allocatedCourseRunAssignmentKeys.length > 1;

return {
isCourseAssigned,
allocatedAssignmentsForCourse,
allocatedCourseRunAssignmentKeys,
allocatedCourseRunAssignments,
hasAssignedCourseRuns,
Expand Down
2 changes: 1 addition & 1 deletion src/components/course/CourseSidebarPrice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const CourseSidebarPrice = () => {
const intl = useIntl();
const { data: enterpriseCustomer } = useEnterpriseCustomer();
const { coursePrice, currency } = useCoursePrice();
const isCourseAssigned = useIsCourseAssigned();
const { isCourseAssigned } = useIsCourseAssigned();
const canRequestSubsidy = useCanUserRequestSubsidyForCourse();
const { userSubsidyApplicableToCourse } = useUserSubsidyApplicableToCourse();

Expand Down
2 changes: 1 addition & 1 deletion src/components/course/course-header/CourseHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const CourseHeader = () => {
const { data: { isPolicyRedemptionEnabled } } = useCourseRedemptionEligibility();
const { data: { containsContentItems } } = useEnterpriseCustomerContainsContent([courseKey]);
const isAssignmentsOnlyLearner = useIsAssignmentsOnlyLearner();
const isCourseAssigned = useIsCourseAssigned();
const { isCourseAssigned } = useIsCourseAssigned();
const isCourseArchived = courseMetadata.courseRuns.every((courseRun) => isArchived(courseRun));
const [partners] = useCoursePartners(courseMetadata);
const defaultProgram = useMemo(
Expand Down
21 changes: 8 additions & 13 deletions src/components/course/course-header/CourseImportantDates.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,17 @@ import {
} from '@openedx/paragon';
import { Calendar } from '@openedx/paragon/icons';
import dayjs from 'dayjs';
import { useParams, useSearchParams } from 'react-router-dom';
import { useSearchParams } from 'react-router-dom';
import { defineMessages, useIntl } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types';
import {
DATE_FORMAT, DATETIME_FORMAT, getSoonestEarliestPossibleExpirationData, hasCourseStarted,
DATE_FORMAT,
DATETIME_FORMAT,
getSoonestEarliestPossibleExpirationData,
hasCourseStarted,
useIsCourseAssigned,
} from '../data';
import {
determineAllocatedCourseRunAssignmentsForCourse,
useCourseMetadata,
useRedeemablePolicies,
} from '../../app/data';
import { useCourseMetadata } from '../../app/data';

const messages = defineMessages({
importantDates: {
Expand Down Expand Up @@ -61,18 +61,13 @@ CourseImportantDate.propTypes = {
};

const CourseImportantDates = () => {
const { courseKey } = useParams();
const { data: redeemableLearnerCreditPolicies } = useRedeemablePolicies();
const { data: courseMetadata } = useCourseMetadata();
const intl = useIntl();
const {
allocatedCourseRunAssignments,
allocatedCourseRunAssignmentKeys,
hasAssignedCourseRuns,
} = determineAllocatedCourseRunAssignmentsForCourse({
redeemableLearnerCreditPolicies,
courseKey,
});
} = useIsCourseAssigned();

const [searchParams] = useSearchParams();
const courseRunKey = searchParams.get('course_run_key')?.replaceAll(' ', '+');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,19 @@ import dayjs from 'dayjs';
import CourseImportantDates from '../CourseImportantDates';
import { renderWithRouterProvider } from '../../../../utils/tests';
import { authenticatedUserFactory } from '../../../app/data/services/data/__factories__';
import { useCourseMetadata, useRedeemablePolicies } from '../../../app/data';
import { COURSE_PACING_MAP, DATE_FORMAT, DATETIME_FORMAT } from '../../data';
import { useCourseMetadata } from '../../../app/data';
import {
COURSE_PACING_MAP,
DATE_FORMAT,
DATETIME_FORMAT,
useIsCourseAssigned,
} from '../../data';
import { TEST_OWNER } from '../../tests/data/constants';

const mockAuthenticatedUser = authenticatedUserFactory();

jest.mock('../../../app/data', () => ({
...jest.requireActual('../../../app/data'),
useRedeemablePolicies: jest.fn(),
useCourseMetadata: jest.fn(),
}));

Expand All @@ -24,6 +28,11 @@ jest.mock('react-router-dom', () => ({
useParams: jest.fn(),
}));

jest.mock('../../data', () => ({
...jest.requireActual('../../data'),
useIsCourseAssigned: jest.fn(),
}));

const futureEarliestExpiration = dayjs().add(5, 'days').toISOString();
const pastEarliestExpiration = dayjs().subtract(5, 'days').toISOString();
const now = dayjs().toISOString();
Expand Down Expand Up @@ -55,40 +64,6 @@ const mockAllocatedAssignments = [{
reason: 'subsidy_expired',
},
}];
const mockLearnerContentAssignments = {
allocatedAssignments: mockAllocatedAssignments,
hasAllocatedAssignments: mockAllocatedAssignments.length > 0,
};

const mockBaseRedeemablePolicies = {
redeemablePolicies: [],
expiredPolicies: [],
unexpiredPolicies: [],
learnerContentAssignments: {
assignments: [],
hasAssignments: false,
allocatedAssignments: [],
hasAllocatedAssignments: false,
acceptedAssignments: [],
hasAcceptedAssignments: false,
canceledAssignments: [],
hasCanceledAssignments: false,
expiredAssignments: [],
hasExpiredAssignments: false,
erroredAssignments: [],
hasErroredAssignments: false,
assignmentsForDisplay: [],
hasAssignmentsForDisplay: false,
},
};

const mockRedeemablePoliciesWithAllocatedAssignments = {
...mockBaseRedeemablePolicies,
learnerContentAssignments: {
...mockBaseRedeemablePolicies.learnerContentAssignments,
...mockLearnerContentAssignments,
},
};

const mockCourseStartDate = dayjs().add(10, 'days').toISOString();

Expand Down Expand Up @@ -132,14 +107,25 @@ describe('<CourseImportantDates />', () => {
beforeEach(() => {
jest.clearAllMocks();
useParams.mockReturnValue({ courseKey: 'edX+DemoX' });
useRedeemablePolicies.mockReturnValue({ data: mockBaseRedeemablePolicies });
useCourseMetadata.mockReturnValue({ data: mockCourseMetadata });
useIsCourseAssigned.mockReturnValue({
allocatedCourseRunAssignments: mockAllocatedAssignments,
allocatedCourseRunAssignmentKeys: mockAllocatedAssignments.map((assignment) => assignment.contentKey),
hasAssignedCourseRuns: mockAllocatedAssignments.length > 0,
});
});
it('renders without crashing', () => {
useRedeemablePolicies.mockReturnValue({
data: mockRedeemablePoliciesWithAllocatedAssignments,

it('does not render without run-based assignments', () => {
useIsCourseAssigned.mockReturnValue({
allocatedCourseRunAssignments: [],
allocatedCourseRunAssignmentKeys: [],
hasAssignedCourseRuns: false,
});
const { container } = renderWithRouterProvider(<CourseImportantDatesWrapper />);
expect(container).toBeEmptyDOMElement();
});

it('renders expected dates for run-based assignments', () => {
renderWithRouterProvider(<CourseImportantDatesWrapper />);

expect(screen.getByText('Important dates')).toBeInTheDocument();
Expand All @@ -149,14 +135,15 @@ describe('<CourseImportantDates />', () => {
expect(screen.getByText(dayjs(now).format(DATETIME_FORMAT))).toBeInTheDocument();
expect(screen.getByText(dayjs(mockCourseStartDate).format(DATE_FORMAT))).toBeInTheDocument();
});

it.each([{
courseStartDate: futureEarliestExpiration,
expected: 'Course starts',
},
{
courseStartDate: pastEarliestExpiration,
expected: 'Course started',
}])('renders the correct tense based on course start date', ({
}])('renders the correct tense based on course start date (%s)', ({
courseStartDate,
expected,
}) => {
Expand All @@ -170,9 +157,6 @@ describe('<CourseImportantDates />', () => {
courseRuns: [updatedMockCourseRun],
availableCourseRuns: [updatedMockCourseRun],
};
useRedeemablePolicies.mockReturnValue({
data: mockRedeemablePoliciesWithAllocatedAssignments,
});
useCourseMetadata.mockReturnValue({ data: updatedMockCourseMetadata });

renderWithRouterProvider(<CourseImportantDatesWrapper />);
Expand Down
4 changes: 2 additions & 2 deletions src/components/course/data/courseLoader.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { generatePath, redirect } from 'react-router-dom';

import {
determineAllocatedCourseRunAssignmentsForCourse,
determineAllocatedAssignmentsForCourse,
determineLearnerHasContentAssignmentsOnly,
extractEnterpriseCustomer,
getCatalogsForSubsidyRequests,
Expand Down Expand Up @@ -80,7 +80,7 @@ const makeCourseLoader: Types.MakeRouteLoaderFunctionWithQueryClient = function
allocatedCourseRunAssignmentKeys,
hasAssignedCourseRuns,
hasMultipleAssignedCourseRuns,
} = determineAllocatedCourseRunAssignmentsForCourse({
} = determineAllocatedAssignmentsForCourse({
courseKey,
redeemableLearnerCreditPolicies,
});
Expand Down
31 changes: 23 additions & 8 deletions src/components/course/data/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
useSubscriptions,
COUPON_CODE_SUBSIDY_TYPE,
LICENSE_SUBSIDY_TYPE,
determineAllocatedAssignmentsForCourse,
} from '../../app/data';
import { LICENSE_STATUS } from '../../enterprise-user-subsidy/data/constants';
import { CourseContext } from '../CourseContextProvider';
Expand Down Expand Up @@ -735,13 +736,27 @@ export const useExternalEnrollmentFailureReason = () => {
* @returns {boolean} - Returns true if the course is assigned to the learner, false otherwise.
*/
export const useIsCourseAssigned = () => {
const { data: { learnerContentAssignments } } = useRedeemablePolicies();
const { data: redeemableLearnerCreditPolicies } = useRedeemablePolicies();
const { data: courseMetadata } = useCourseMetadata();
if (!learnerContentAssignments.hasAllocatedAssignments) {
return false;
}
const isCourseAssigned = learnerContentAssignments.allocatedAssignments.some(
(assignment) => assignment.contentKey === courseMetadata.key,
);
return isCourseAssigned;

const {
isCourseAssigned,
allocatedAssignmentsForCourse,
allocatedCourseRunAssignmentKeys,
allocatedCourseRunAssignments,
hasAssignedCourseRuns,
hasMultipleAssignedCourseRuns,
} = determineAllocatedAssignmentsForCourse({
courseKey: courseMetadata.key,
redeemableLearnerCreditPolicies,
});

return {
isCourseAssigned,
allocatedAssignmentsForCourse,
allocatedCourseRunAssignmentKeys,
allocatedCourseRunAssignments,
hasAssignedCourseRuns,
hasMultipleAssignedCourseRuns,
};
};
Loading

0 comments on commit 0d978d2

Please sign in to comment.