Skip to content

Commit

Permalink
Merge pull request #102 from edx/azan/PROD-2257
Browse files Browse the repository at this point in the history
Add Licenses Subscription Data for Users
  • Loading branch information
azanbinzahid authored Apr 9, 2021
2 parents 0760f3d + 2876c28 commit 1ecdd01
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 2 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ CSRF_TOKEN_API_PATH=null
ECOMMERCE_BASE_URL=null
LANGUAGE_PREFERENCE_COOKIE_NAME=null
LMS_BASE_URL=null
LICENSE_MANAGER_URL=null
DISCOVERY_API_BASE_URL=null
LOGIN_URL=null
LOGOUT_URL=null
Expand Down
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LICENSE_MANAGER_URL='http://localhost:18170'
DISCOVERY_API_BASE_URL='http://localhost:18381'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
Expand Down
1 change: 1 addition & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
ECOMMERCE_BASE_URL='http://localhost:18130'
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
LMS_BASE_URL='http://localhost:18000'
LICENSE_MANAGER_URL='http://localhost:18170'
DISCOVERY_API_BASE_URL='http://localhost:18381'
LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
Expand Down
7 changes: 6 additions & 1 deletion src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import 'babel-polyfill';

import {
APP_INIT_ERROR, APP_READY, subscribe, initialize,
APP_INIT_ERROR, APP_READY, subscribe, initialize, mergeConfig,
} from '@edx/frontend-platform';
import { AppProvider, ErrorPage } from '@edx/frontend-platform/react';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';

import React from 'react';
import ReactDOM from 'react-dom';
import { Switch, Route } from 'react-router-dom';
Expand All @@ -17,6 +18,10 @@ import UserMessagesProvider from './user-messages/UserMessagesProvider';

import './index.scss';

mergeConfig({
LICENSE_MANAGER_URL: process.env.LICENSE_MANAGER_URL,
});

subscribe(APP_READY, () => {
const { administrator } = getAuthenticatedUser();
if (!administrator) {
Expand Down
4 changes: 4 additions & 0 deletions src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { MockAuthService } from '@edx/frontend-platform/auth';

Enzyme.configure({ adapter: new Adapter() });

mergeConfig({
LICENSE_MANAGER_URL: process.env.LICENSE_MANAGER_URL,
});

initialize({
handlers: {
config: () => {
Expand Down
145 changes: 145 additions & 0 deletions src/users/Licenses.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import React, {
useMemo,
useState,
useCallback,
} from 'react';
import PropTypes from 'prop-types';
import { Collapsible, Badge } from '@edx/paragon';
import sort from './sort';
import Table from '../Table';
import formatDate from '../dates/formatDate';

export default function Licenses({
data, status, expanded,
}) {
const [sortColumn, setSortColumn] = useState('status');
const [sortDirection, setSortDirection] = useState('desc');
const responseStatus = useMemo(() => status, [status]);
const tableData = useMemo(() => {
if (data === null || data.length === 0) {
return [];
}
return data.map(result => ({
status: {
value: result.status,
},
assignedDate: {
displayValue: formatDate(result.assignedDate),
value: result.assignedDate,
},
activationDate: {
displayValue: formatDate(result.activationDate),
value: result.activationDate,
},
revokedDate: {
displayValue: formatDate(result.revokedDate),
value: result.revokedDate,
},
lastRemindDate: {
displayValue: formatDate(result.lastRemindDate),
value: result.lastRemindDate,
},
subscriptionPlanTitle: {
value: result.subscriptionPlanTitle,
},
subscriptionPlanExpirationDate: {
displayValue: formatDate(result.subscriptionPlanExpirationDate),
value: result.subscriptionPlanExpirationDate,
},
activationLink: {
displayValue: <a href={result.activationLink} rel="noopener noreferrer" target="_blank" className="word_break">{result.activationLink}</a>,
value: result.activationLink,
},
}));
}, [data]);

const setSort = useCallback((column) => {
if (sortColumn === column) {
setSortDirection(sortDirection === 'desc' ? 'asc' : 'desc');
} else {
setSortDirection('desc');
}
setSortColumn(column);
});
const columns = [

{
label: 'Status', key: 'status', columnSortable: true, onSort: () => setSort('status'), width: 'col-3',
},
{
label: 'Assigned Date', key: 'assignedDate', columnSortable: true, onSort: () => setSort('assignedDate'), width: 'col-3',
},
{
label: 'Activation Date', key: 'activationDate', columnSortable: true, onSort: () => setSort('activationDate'), width: 'col-3',
},
{
label: 'Revoked Date', key: 'revokedDate', columnSortable: true, onSort: () => setSort('revokedDate'), width: 'col-3',
},
{
label: 'Last Remind Date', key: 'lastRemindDate', columnSortable: true, onSort: () => setSort('lastRemindDate'), width: 'col-3',
},
{
label: 'Subscription Plan Title', key: 'subscriptionPlanTitle', columnSortable: true, onSort: () => setSort('subscriptionPlanTitle'), width: 'col-3',
},
{
label: 'Subscription Plan Expiration Date', key: 'subscriptionPlanExpirationDate', columnSortable: true, onSort: () => setSort('subscriptionPlanExpirationDate'), width: 'col-3',
},
{
label: 'Activation Link', key: 'activationLink', columnSortable: true, onSort: () => setSort('activationLink'), width: 'col-3',
},
];

const tableDataSortable = [...tableData];
let statusMsg;
if (responseStatus !== '') {
statusMsg = <Badge variant="light">{`Fetch Status: ${responseStatus}`}</Badge>;
} else {
statusMsg = null;
}

return (
<section className="mb-3">
<Collapsible
title={(
<>
{`Licenses (${tableData.length})`}
{statusMsg}
</>
)}
defaultOpen={expanded}
>
<Table
className="w-auto"
data={tableDataSortable.sort(
(firstElement, secondElement) => sort(firstElement, secondElement, sortColumn, sortDirection),
)}
columns={columns}
tableSortable
defaultSortedColumn="status"
defaultSortDirection="desc"
/>
</Collapsible>
</section>
);
}

Licenses.propTypes = {
data: PropTypes.arrayOf(PropTypes.shape({
status: PropTypes.string,
assignedDate: PropTypes.string,
revokedDate: PropTypes.string,
activationDate: PropTypes.string,
subscriptionPlanTitle: PropTypes.string,
lastRemindDate: PropTypes.string,
activationLink: PropTypes.string,
subscriptionPlanExpirationDate: PropTypes.string,
})),
status: PropTypes.string,
expanded: PropTypes.bool,
};

Licenses.defaultProps = {
data: null,
status: '',
expanded: false,
};
48 changes: 48 additions & 0 deletions src/users/Licenses.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { mount } from 'enzyme';
import React from 'react';

import Licenses from './Licenses';
import licensesData from './data/test/licenses';
import UserMessagesProvider from '../user-messages/UserMessagesProvider';

const LicensesPageWrapper = (props) => (
<UserMessagesProvider>
<Licenses {...props} />
</UserMessagesProvider>
);

describe('User Licenses Listing', () => {
let wrapper;

beforeEach(() => {
wrapper = mount(<LicensesPageWrapper {...licensesData} />);
});

it('default collapsible with enrollment data', () => {
const collapsible = wrapper.find('CollapsibleAdvanced').find('.collapsible-trigger').hostNodes();
expect(collapsible.text()).toEqual('Licenses (2)');
});

it('No License Data', () => {
const licenseData = { ...licensesData, data: [], status: 'No record found' };
wrapper = mount(<Licenses {...licenseData} />);
const collapsible = wrapper.find('CollapsibleAdvanced').find('.collapsible-trigger').hostNodes();
expect(collapsible.text()).toEqual('Licenses (0)Fetch Status: No record found');
});

it('Sorting Columns Button Enabled by default', () => {
const dataTable = wrapper.find('table.table');
const tableHeaders = dataTable.find('thead tr th');

tableHeaders.forEach(header => {
const sortButton = header.find('button.btn-header');
expect(sortButton.disabled).toBeFalsy();
});
});

it('Table Header Lenght', () => {
const dataTable = wrapper.find('table.table');
const tableHeaders = dataTable.find('thead tr th');
expect(tableHeaders).toHaveLength(Object.keys(licensesData.data[0]).length);
});
});
11 changes: 11 additions & 0 deletions src/users/UserPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import UserMessagesContext from '../user-messages/UserMessagesContext';
import { isEmail, isValidUsername } from '../utils/index';
import { getAllUserData } from './data/api';
import Enrollments from './Enrollments';
import Licenses from './Licenses';
import Entitlements from './entitlements/Entitlements';
import UserSearch from './UserSearch';
import UserSummary from './UserSummary';
Expand All @@ -27,6 +28,7 @@ export default function UserPage({ location }) {
const [loading, setLoading] = useState(false);
const [showEnrollments, setShowEnrollments] = useState(true);
const [showEntitlements, setShowEntitlements] = useState(false);
const [showLicenses, setShowLicenses] = useState(false);
const { add, clear } = useContext(UserMessagesContext);

function pushHistoryIfChanged(nextUrl) {
Expand Down Expand Up @@ -93,6 +95,7 @@ export default function UserPage({ location }) {
setSearching(true);
setShowEntitlements(false);
setShowEnrollments(true);
setShowLicenses(false);
handleFetchSearchResults(searchValue);
});

Expand All @@ -103,12 +106,14 @@ export default function UserPage({ location }) {

const handleEntitlementsChange = useCallback(() => {
setShowEntitlements(true);
setShowLicenses(true);
setShowEnrollments(false);
handleFetchSearchResults(userIdentifier);
});

const handleEnrollmentsChange = useCallback(() => {
setShowEntitlements(false);
setShowLicenses(false);
setShowEnrollments(true);
handleFetchSearchResults(userIdentifier);
});
Expand Down Expand Up @@ -149,6 +154,11 @@ export default function UserPage({ location }) {
ssoRecords={data.ssoRecords}
changeHandler={handleUserSummaryChange}
/>
<Licenses
data={data.licenses.results}
status={data.licenses.status}
expanded={showLicenses}
/>
<Entitlements
user={data.user.username}
data={data.entitlements}
Expand All @@ -161,6 +171,7 @@ export default function UserPage({ location }) {
changeHandler={handleEnrollmentsChange}
expanded={showEnrollments}
/>

</>
)}
{!loading && !userIdentifier && (
Expand Down
43 changes: 42 additions & 1 deletion src/users/data/api.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getConfig } from '@edx/frontend-platform';
import { getConfig, ensureConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as messages from '../../user-messages/messages';
import { isEmail, isValidUsername } from '../../utils/index';
Expand Down Expand Up @@ -156,13 +156,52 @@ export async function getUserPasswordStatus(userIdentifier) {
return data;
}

ensureConfig([
'LICENSE_MANAGER_URL',
], 'getLicense');

export async function getLicense(userEmail) {
const defaultResponse = {
status: '',
results: [],
};
try {
const { data } = await getAuthenticatedHttpClient().post(
`${getConfig().LICENSE_MANAGER_URL}/api/v1/staff_lookup_licenses/`,
{ user_email: userEmail },
);
defaultResponse.results = data;
return defaultResponse;
} catch (error) {
let errorStatus = -1;

if ('customAttributes' in error) {
errorStatus = error.customAttributes.httpErrorStatus;
}

if (errorStatus === 404) {
defaultResponse.status = 'No record found';
} else if (errorStatus === 400) {
defaultResponse.status = 'User email is not provided';
} else if (errorStatus === 403) {
defaultResponse.status = 'Forbidden: User does not have permission to view this data';
} else if (errorStatus === 401) {
defaultResponse.status = 'Unauthenticated: Could not autheticate user to view this data';
} else {
defaultResponse.status = 'Unable to connect to the service';
}
return defaultResponse;
}
}

export async function getAllUserData(userIdentifier) {
const errors = [];
let user = null;
let entitlements = [];
let enrollments = [];
let verificationStatus = null;
let ssoRecords = null;
let licenses = [];
try {
user = await getUser(userIdentifier);
} catch (error) {
Expand All @@ -178,6 +217,7 @@ export async function getAllUserData(userIdentifier) {
verificationStatus = await getUserVerificationStatus(user.username);
ssoRecords = await getSsoRecords(user.username);
user.passwordStatus = await getUserPasswordStatus(user.username);
licenses = await getLicense(user.email);
}

return {
Expand All @@ -187,6 +227,7 @@ export async function getAllUserData(userIdentifier) {
enrollments,
verificationStatus,
ssoRecords,
licenses,
};
}

Expand Down
Loading

0 comments on commit 1ecdd01

Please sign in to comment.