From 6abb49d12505269063bd5c2e134ca4d9389b3cd0 Mon Sep 17 00:00:00 2001 From: Troy Sankey Date: Fri, 18 Oct 2024 09:45:14 -0700 Subject: [PATCH 01/21] temp: hide restricted runs before we're ready to show them ENT-9505 --- src/components/learner-credit-management/data/utils.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index ba7272086..38d47c3ff 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -698,7 +698,7 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, })); const assignableCourseRunsFilter = ({ - enrollBy, enrollStart, start, hasEnrollBy, hasEnrollStart, isActive, isLateEnrollmentEligible, + enrollBy, enrollStart, start, hasEnrollBy, hasEnrollStart, isActive, isLateEnrollmentEligible, restrictionType, }) => { const isEnrollByDateValid = isEnrollByDateWithinThreshold({ hasEnrollBy, @@ -727,6 +727,14 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, // to do is make sure the run itself is generally eligible for late enrollment return isLateEnrollmentEligible; } + // ENT-9359 (epic for Custom Presentations/Restricted Runs): + // Temporarily hide all restricted runs unconditionally on the run assignment + // dropdown during implementation of the overall feature. ENT-9411 is most likely + // the ticket to replace this code with something to actually show restricted + // runs conditionally. + if (restrictionType) { + return false; + } // General courseware filter return isActive; }; From 888fb6b2f0c50558b8640e572a9072eb2a5c8b7e Mon Sep 17 00:00:00 2001 From: Alexander Dusenbery Date: Tue, 22 Oct 2024 13:49:25 -0400 Subject: [PATCH 02/21] temp: move restrictionType check prior to check for late redemption --- .../data/tests/utils.test.js | 39 +++++++++++++++++++ .../learner-credit-management/data/utils.js | 14 +++---- 2 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/components/learner-credit-management/data/tests/utils.test.js b/src/components/learner-credit-management/data/tests/utils.test.js index bcfb62022..9e0601403 100644 --- a/src/components/learner-credit-management/data/tests/utils.test.js +++ b/src/components/learner-credit-management/data/tests/utils.test.js @@ -1,6 +1,7 @@ import { createIntl } from '@edx/frontend-platform/i18n'; import dayjs from 'dayjs'; import { + getAssignableCourseRuns, getBudgetStatus, getTranslatedBudgetStatus, getTranslatedBudgetTerm, @@ -343,3 +344,41 @@ describe('startAndEnrollBySortLogic', () => { expect(sortedDates).toEqual(sampleData.sort((a, b) => a.expectedOrder - b.expectedOrder)); }); }); + +describe('getAssignableCourseRuns', () => { + it('includes a late, non-restricted course run when late-redemption eligible', () => { + const courseRuns = [ + { + key: 'the-course-run', + enrollBy: dayjs().subtract(1, 'day'), + hasEnrollBy: true, + upgradeDeadline: dayjs().add(1, 'day'), + start: dayjs().subtract(1, 'day'), + isActive: true, + }, + ]; + const subsidyExpirationDatetime = dayjs().add(100, 'day'); + const isLateRedemptionAllowed = true; + + const result = getAssignableCourseRuns({ courseRuns, subsidyExpirationDatetime, isLateRedemptionAllowed }); + expect(result.length).toEqual(1); + expect(result[0].key).toEqual('the-course-run'); + }); + it('returns an empty list given only a restricted run , even when late-redemption eligible', () => { + const courseRuns = [ + { + enrollBy: dayjs().subtract(1, 'day'), + hasEnrollBy: true, + restrictionType: 'b2b-enterprise', + upgradeDeadline: dayjs().subtract(1, 'day'), + start: dayjs().subtract(1, 'day'), + isActive: true, + }, + ]; + const subsidyExpirationDatetime = dayjs().add(100, 'day'); + const isLateRedemptionAllowed = true; + + const result = getAssignableCourseRuns({ courseRuns, subsidyExpirationDatetime, isLateRedemptionAllowed }); + expect(result).toEqual([]); + }); +}); diff --git a/src/components/learner-credit-management/data/utils.js b/src/components/learner-credit-management/data/utils.js index 38d47c3ff..c8a04981e 100644 --- a/src/components/learner-credit-management/data/utils.js +++ b/src/components/learner-credit-management/data/utils.js @@ -720,13 +720,6 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, // the current date and subsidy expiration date have failed. return false; } - if (hasEnrollBy && isLateRedemptionAllowed && isDateBeforeToday(enrollBy)) { - // Special case: late enrollment has been enabled by ECS for this budget, and - // isEligibleForEnrollment already succeeded, so we know that late enrollment - // would be happy given enrollment deadline of the course. Now all we need - // to do is make sure the run itself is generally eligible for late enrollment - return isLateEnrollmentEligible; - } // ENT-9359 (epic for Custom Presentations/Restricted Runs): // Temporarily hide all restricted runs unconditionally on the run assignment // dropdown during implementation of the overall feature. ENT-9411 is most likely @@ -735,6 +728,13 @@ export const getAssignableCourseRuns = ({ courseRuns, subsidyExpirationDatetime, if (restrictionType) { return false; } + if (hasEnrollBy && isLateRedemptionAllowed && isDateBeforeToday(enrollBy)) { + // Special case: late enrollment has been enabled by ECS for this budget, and + // isEligibleForEnrollment already succeeded, so we know that late enrollment + // would be happy given enrollment deadline of the course. Now all we need + // to do is make sure the run itself is generally eligible for late enrollment + return isLateEnrollmentEligible; + } // General courseware filter return isActive; }; From 8a30273fd59fbbbac5a18ac1140527cb5439b279 Mon Sep 17 00:00:00 2001 From: muhammad-ammar Date: Thu, 17 Oct 2024 11:55:39 +0500 Subject: [PATCH 03/21] feat: lpr budgets filtering --- src/components/Admin/Admin.test.jsx | 31 + src/components/Admin/AdminSearchForm.jsx | 64 +- src/components/Admin/AdminSearchForm.test.jsx | 33 + .../Admin/__snapshots__/Admin.test.jsx.snap | 2377 +++++++++++++++-- src/components/Admin/index.jsx | 12 +- src/containers/AdminPage/AdminPage.test.jsx | 4 + src/containers/AdminPage/index.jsx | 9 + src/data/actions/enterpriseBudgets.js | 41 + src/data/constants/enterpriseBudgets.js | 11 + src/data/reducers/enterpriseBudgets.js | 47 + src/data/reducers/enterpriseBudgets.test.js | 80 + src/data/reducers/index.js | 2 + src/data/services/EnterpriseDataApiService.js | 6 + src/utils.js | 3 + 14 files changed, 2555 insertions(+), 165 deletions(-) create mode 100644 src/data/actions/enterpriseBudgets.js create mode 100644 src/data/constants/enterpriseBudgets.js create mode 100644 src/data/reducers/enterpriseBudgets.js create mode 100644 src/data/reducers/enterpriseBudgets.test.js diff --git a/src/components/Admin/Admin.test.jsx b/src/components/Admin/Admin.test.jsx index c0a1105c7..0cd7685cf 100644 --- a/src/components/Admin/Admin.test.jsx +++ b/src/components/Admin/Admin.test.jsx @@ -54,6 +54,10 @@ const store = mockStore({ loading: null, insights: null, }, + enterpriseBudgets: { + loading: null, + budgets: null, + }, }); const AdminWrapper = props => ( @@ -78,8 +82,14 @@ const AdminWrapper = props => ( pathname: '/', }} {...props} + budgets={[{ + subsidy_access_policy_uuid: '8d6503dd-e40d-42b8-442b-37dd4c5450e3', + subsidy_access_policy_display_name: 'Everything', + }]} fetchDashboardInsights={() => {}} clearDashboardInsights={() => {}} + fetchEnterpriseBudgets={() => {}} + clearEnterpriseBudgets={() => {}} /> @@ -97,6 +107,7 @@ describe('', () => { lastUpdatedDate: '2018-07-31T23:14:35Z', numberOfUsers: 3, insights: null, + budgets: [], }; describe('renders correctly', () => { @@ -370,6 +381,26 @@ describe('', () => { expect(tree).toMatchSnapshot(); }); }); + + describe('with enterprise budgets data', () => { + it('renders budgets correctly', () => { + const budgetUUID = '8d6503dd-e40d-42b8-442b-37dd4c5450e3'; + const budgets = [{ + subsidy_access_policy_uuid: budgetUUID, + subsidy_access_policy_display_name: 'Everything', + }]; + const tree = renderer + .create(( + + )) + .toJSON(); + + expect(tree).toMatchSnapshot(); + }); + }); }); describe('handle changes to enterpriseId prop', () => { diff --git a/src/components/Admin/AdminSearchForm.jsx b/src/components/Admin/AdminSearchForm.jsx index a2624cef8..e93626f16 100644 --- a/src/components/Admin/AdminSearchForm.jsx +++ b/src/components/Admin/AdminSearchForm.jsx @@ -1,6 +1,7 @@ /* eslint-disable camelcase */ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { Form } from '@openedx/paragon'; import { Info } from '@openedx/paragon/icons'; @@ -14,17 +15,22 @@ import { withLocation, withNavigate } from '../../hoc'; class AdminSearchForm extends React.Component { componentDidUpdate(prevProps) { - const { searchParams: { searchQuery, searchCourseQuery, searchDateQuery } } = this.props; + const { + searchParams: { + searchQuery, searchCourseQuery, searchDateQuery, searchBudgetQuery, + }, + } = this.props; const { searchParams: { searchQuery: prevSearchQuery, searchCourseQuery: prevSearchCourseQuery, searchDateQuery: prevSearchDateQuery, + searchBudgetQuery: prevSearchBudgetQuery, }, } = prevProps; if (searchQuery !== prevSearchQuery || searchCourseQuery !== prevSearchCourseQuery - || searchDateQuery !== prevSearchDateQuery) { + || searchDateQuery !== prevSearchDateQuery || searchBudgetQuery !== prevSearchBudgetQuery) { this.handleSearch(); } } @@ -45,14 +51,27 @@ class AdminSearchForm extends React.Component { updateUrl(navigate, location.pathname, updateParams); } + onBudgetSelect(event) { + const { navigate, location } = this.props; + const updateParams = { + budget_uuid: event.target.value, + page: 1, + }; + updateUrl(navigate, location.pathname, updateParams); + } + render() { const { intl, tableData, - searchParams: { searchCourseQuery, searchDateQuery, searchQuery }, + budgets, + searchParams: { + searchCourseQuery, searchDateQuery, searchQuery, searchBudgetQuery, + }, } = this.props; const courseTitles = Array.from(new Set(tableData.map(en => en.course_title).sort())); const courseDates = Array.from(new Set(tableData.map(en => en.course_start_date).sort().reverse())); + const columnWidth = budgets?.length ? 'col-md-3' : 'col-md-6'; return (
@@ -151,7 +170,7 @@ class AdminSearchForm extends React.Component {
-
+
+ {budgets?.length && ( +
+ + + + + this.onBudgetSelect(e)} + > + + {budgets.map(budget => ( + + ))} + + +
+ )}
@@ -193,8 +247,10 @@ AdminSearchForm.propTypes = { searchQuery: PropTypes.string, searchCourseQuery: PropTypes.string, searchDateQuery: PropTypes.string, + searchBudgetQuery: PropTypes.string, }).isRequired, tableData: PropTypes.arrayOf(PropTypes.shape({})), + budgets: PropTypes.arrayOf(PropTypes.shape({})), navigate: PropTypes.func, location: PropTypes.shape({ pathname: PropTypes.string, diff --git a/src/components/Admin/AdminSearchForm.test.jsx b/src/components/Admin/AdminSearchForm.test.jsx index 7a0d944eb..dada76b68 100644 --- a/src/components/Admin/AdminSearchForm.test.jsx +++ b/src/components/Admin/AdminSearchForm.test.jsx @@ -5,6 +5,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import AdminSearchForm from './AdminSearchForm'; import SearchBar from '../SearchBar'; +import { updateUrl } from '../../utils'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -12,6 +13,10 @@ jest.mock('react-router-dom', () => ({ useNavigate: jest.fn(), })); +jest.mock('../../utils', () => ({ + updateUrl: jest.fn(), +})); + const DEFAULT_PROPS = { searchEnrollmentsList: () => {}, searchParams: {}, @@ -48,4 +53,32 @@ describe('', () => { expect(spy).toHaveBeenCalledTimes(1); }); }); + + it('select the correct budget', () => { + const budgetUUID = '8d6503dd-e40d-42b8-442b-37dd4c5450e3'; + const budgets = [{ + subsidy_access_policy_uuid: budgetUUID, + subsidy_access_policy_display_name: 'Everything', + }]; + const props = { + ...DEFAULT_PROPS, + budgets, + location: { pathname: '/admin/learners' }, + }; + const wrapper = mount( + , + ); + const selectElement = wrapper.find('.budgets-dropdown select'); + + selectElement.simulate('change', { target: { value: budgetUUID } }); + expect(updateUrl).toHaveBeenCalled(); + expect(updateUrl).toHaveBeenCalledWith( + undefined, + '/admin/learners', + { + budget_uuid: budgetUUID, + page: 1, + }, + ); + }); }); diff --git a/src/components/Admin/__snapshots__/Admin.test.jsx.snap b/src/components/Admin/__snapshots__/Admin.test.jsx.snap index 234d84211..5b2458ef5 100644 --- a/src/components/Admin/__snapshots__/Admin.test.jsx.snap +++ b/src/components/Admin/__snapshots__/Admin.test.jsx.snap @@ -1665,7 +1665,7 @@ exports[` renders correctly with dashboard analytics data renders # cou > @@ -1674,7 +1674,7 @@ exports[` renders correctly with dashboard analytics data renders # cou > renders correctly with dashboard analytics data renders # cou
+
+
+ +
+ +
+
+
@@ -1855,7 +1891,7 @@ exports[` renders correctly with dashboard analytics data renders # cou >