Skip to content

Commit

Permalink
Merge pull request #126 from edx/azan/PROD-2358
Browse files Browse the repository at this point in the history
feat: show onboarding status for user
  • Loading branch information
azanbinzahid authored Jun 22, 2021
2 parents a3e2de5 + b1d3d11 commit e46dc50
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 5 deletions.
1 change: 1 addition & 0 deletions src/users/UserPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export default function UserPage({ location }) {
userData={data.user}
verificationData={data.verificationStatus}
ssoRecords={data.ssoRecords}
onboardingData={data.onboardingStatus}
changeHandler={handleUserSummaryChange}
/>
<Licenses
Expand Down
49 changes: 48 additions & 1 deletion src/users/UserSummary.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Modal, Button, Input } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import { postTogglePasswordStatus, postResetPassword } from './data/api';
import Table from '../Table';
import { formatDate } from '../utils';
import { formatDate, titleCase } from '../utils';
import { getAccountActivationUrl } from './data/urls';

export default function UserSummary({
userData,
verificationData,
ssoRecords,
onboardingData,
changeHandler,
}) {
const [ssoModalIsOpen, setSsoModalIsOpen] = useState(false);
Expand Down Expand Up @@ -151,6 +153,7 @@ export default function UserSummary({
key: 'extra',
},
];

const userPasswordHistoryColumns = [
{
label: 'Date',
Expand Down Expand Up @@ -241,6 +244,35 @@ export default function UserSummary({
},
}));

const proctoringData = [onboardingData].map(result => ({
status: result.onboardingStatus ? titleCase(result.onboardingStatus) : 'Not Started',
expirationDate: formatDate(result.expirationDate),
onboardingReleaseDate: formatDate(result.onboardingReleaseDate),
onboardingLink: result.onboardingLink ? {
displayValue: <a href={`${getConfig().LMS_BASE_URL}${result.onboardingLink}`} rel="noopener noreferrer" target="_blank" className="word_break">Link</a>,
value: result.onboardingLink,
} : 'N/A',
}));

const proctoringColumns = [
{
label: 'Onboarding Status',
key: 'status',
},
{
label: 'Expiration Date',
key: 'expirationDate',
},
{
label: 'Release Date',
key: 'onboardingReleaseDate',
},
{
label: 'Onboarding Link',
key: 'onboardingLink',
},
];

if (!userData.isActive) {
let dataValue;
if (userData.activationKey !== null) {
Expand Down Expand Up @@ -306,6 +338,14 @@ export default function UserSummary({
columns={idvColumns}
/>
</div>
<div className="flex-column p-4 m-3 card">
<h4>Proctoring Information</h4>
<Table
id="proctoring-data"
data={proctoringData}
columns={proctoringColumns}
/>
</div>
<div className="flex-column p-4 m-3 card">
<h4>SSO Records</h4>
<Table
Expand Down Expand Up @@ -428,11 +468,18 @@ UserSummary.propTypes = {
extraData: PropTypes.shape([]),
}),
ssoRecords: PropTypes.shape([]),
onboardingData: PropTypes.shape({
onboardingStatus: PropTypes.string,
expirationDate: PropTypes.string,
onboardingLink: PropTypes.string,
onboardingReleaseDate: PropTypes.string,
}),
changeHandler: PropTypes.func.isRequired,
};

UserSummary.defaultProps = {
userData: null,
verificationData: null,
ssoRecords: [],
onboardingData: null,
};
35 changes: 35 additions & 0 deletions src/users/UserSummary.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import React from 'react';
import * as api from './data/api';
import UserSummary from './UserSummary';
import UserSummaryData from './data/test/userSummary';
import { formatDate, titleCase } from '../utils';

const getActivationKeyRow = (data) => {
const wrapper = mount(<UserSummary {...data} />);
Expand Down Expand Up @@ -60,6 +61,40 @@ describe('User Summary Component Tests', () => {
expect(ComponentSsoData.modified).toEqual(ExpectedSsoData.modified);
expect(ComponentSsoData.extraData).toEqual(ExpectedSsoData.extraData);
});
it('Onboarding Status Data Values', () => {
const ComponentOnboardingData = wrapper.prop('onboardingData');
const ExpectedOnboardingData = UserSummaryData.onboardingData;

expect(ComponentOnboardingData.onboardingStatus).toEqual(ExpectedOnboardingData.onboardingStatus);
expect(ComponentOnboardingData.expirationDate).toEqual(ExpectedOnboardingData.expirationDate);
expect(ComponentOnboardingData.onboardingReleaseDate).toEqual(ExpectedOnboardingData.onboardingReleaseDate);
expect(ComponentOnboardingData.onboardingLink).toEqual(ExpectedOnboardingData.onboardingLink);
});
});

describe('Onboarding Status Data', () => {
it('Onboarding Status', () => {
const dataTable = wrapper.find('Table#proctoring-data');
const dataBody = dataTable.find('tbody tr td');
expect(dataBody).toHaveLength(4);
expect(dataBody.at(0).text()).toEqual(titleCase(UserSummaryData.onboardingData.onboardingStatus));
expect(dataBody.at(1).text()).toEqual(formatDate(UserSummaryData.onboardingData.expirationDate));
expect(dataBody.at(2).text()).toEqual(formatDate(UserSummaryData.onboardingData.onboardingReleaseDate));
expect(dataBody.at(3).text()).toEqual('Link');
});

it('No Onboarding Status Data', () => {
const onboardingData = { ...UserSummaryData.onboardingData, onboardingStatus: null, onboardingLink: null };
const userData = { ...UserSummaryData, onboardingData };
wrapper = mount(<UserSummary {...userData} />);
const dataTable = wrapper.find('Table#proctoring-data');
const dataBody = dataTable.find('tbody tr td');
expect(dataBody).toHaveLength(4);
expect(dataBody.at(0).text()).toEqual('Not Started');
expect(dataBody.at(1).text()).toEqual(formatDate(UserSummaryData.onboardingData.expirationDate));
expect(dataBody.at(2).text()).toEqual(formatDate(UserSummaryData.onboardingData.onboardingReleaseDate));
expect(dataBody.at(3).text()).toEqual('N/A');
});
});

describe('Registration Activation Field', () => {
Expand Down
46 changes: 45 additions & 1 deletion src/users/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { ensureConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as messages from '../../userMessages/messages';
import * as AppUrls from './urls';
import { isEmail } from '../../utils/index';
import { isEmail } from '../../utils';

export async function getEntitlements(username, page = 1) {
const baseURL = AppUrls.getEntitlementUrl();
Expand Down Expand Up @@ -175,6 +175,47 @@ export async function getLicense(userEmail) {
}
}

export async function getOnboardingStatus(enrollments, username) {
const defaultResponse = {
onboardingStatus: null,
expirationDate: null,
onboardingLink: null,
onboardingPastDue: null,
onboardingReleaseDate: null,
reviewRequirementsUrl: null,
};

// get most recent paid enrollment
const paidEnrollments = enrollments.filter((course) => course.mode === 'verified' || course.mode === 'professional');
paidEnrollments.sort((a, b) => (a.created < b.created));

if (paidEnrollments.length === 0) {
return {
...defaultResponse,
onboardingStatus: 'No Paid Enrollment',
};
}

const courseId = paidEnrollments[0].course_id;
try {
const { data } = await getAuthenticatedHttpClient().get(
AppUrls.getOnboardingStatusUrl(courseId, username),
);
return data;
} catch (error) {
if ('customAttributes' in error && error.customAttributes.httpErrorStatus === 404) {
return {
...defaultResponse,
onboardingStatus: 'No Record Found',
};
}
return {
...defaultResponse,
onboardingStatus: 'Error while fetching data',
};
}
}

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

return {
Expand All @@ -209,6 +252,7 @@ export async function getAllUserData(userIdentifier) {
verificationStatus,
ssoRecords,
licenses,
onboardingStatus,
};
}

Expand Down
54 changes: 52 additions & 2 deletions src/users/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('API', () => {
const verificationDetailsApiUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${testUsername}/verifications/`;
const verificationStatusApiUrl = `${getConfig().LMS_BASE_URL}/api/user/v1/accounts/${testUsername}/verification_status/`;
const licensesApiUrl = `${getConfig().LICENSE_MANAGER_URL}/api/v1/staff_lookup_licenses/`;
const onboardingStatusApiUrl = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/user_onboarding/status`;

let mockAdapter;

Expand All @@ -36,6 +37,44 @@ describe('API', () => {
mockAdapter.reset();
});

describe('Onboarding Status Fetch', () => {
const expectedSuccessResponse = {
onboardingStatus: 'verified',
expirationDate: null,
onboardingLink: null,
onboardingPastDue: null,
onboardingReleaseDate: null,
reviewRequirementsUrl: null,
};

// prepare enrollments data
const { data } = enrollmentsData;
data[1].mode = 'verified';
data[1].course_id = data[1].courseId;
const url = `${onboardingStatusApiUrl}?course_id=${encodeURIComponent(data[1].courseId)}&username=${encodeURIComponent(testUsername)}`;

it('Successful Fetch ', async () => {
mockAdapter.onGet(url).reply(200, expectedSuccessResponse);

const response = await api.getOnboardingStatus(data, testUsername);
expect(response).toEqual(expectedSuccessResponse);
});

it('No Record for Onboaring Status Fetch ', async () => {
mockAdapter.onGet(url).reply(() => throwError(404, ''));

const response = await api.getOnboardingStatus(data, testUsername);
expect(response).toEqual({ ...expectedSuccessResponse, onboardingStatus: 'No Record Found' });
});

it('Unexpected error', async () => {
mockAdapter.onGet(url).reply(() => throwError(500, ''));

const response = await api.getOnboardingStatus(data, testUsername);
expect(response).toEqual({ ...expectedSuccessResponse, onboardingStatus: 'Error while fetching data' });
});
});

describe('SSO Records Fetch', () => {
it('No SSO data is Returned', async () => {
mockAdapter.onGet(ssoRecordsApiUrl).reply(200, []);
Expand Down Expand Up @@ -265,6 +304,15 @@ describe('API', () => {
is_active: true,
};

const onboardingDefaultResponse = {
onboardingStatus: null,
expirationDate: null,
onboardingLink: null,
onboardingPastDue: null,
onboardingReleaseDate: null,
reviewRequirementsUrl: null,
};

it('Unsuccessful User Data Retrieval', async () => {
const expectedUserError = {
code: null,
Expand All @@ -284,22 +332,24 @@ describe('API', () => {
it('Successful User Data Retrieval', async () => {
mockAdapter.onGet(`${userAccountApiBaseUrl}/${testUsername}`).reply(200, successDictResponse);
mockAdapter.onGet(`${entitlementsApiBaseUrl}&page=1`).reply(200, { results: [], next: null });
mockAdapter.onGet(enrollmentsApiUrl).reply(200, {});
mockAdapter.onGet(enrollmentsApiUrl).reply(200, []);
mockAdapter.onGet(ssoRecordsApiUrl).reply(200, []);
mockAdapter.onGet(verificationDetailsApiUrl).reply(200, {});
mockAdapter.onGet(verificationStatusApiUrl).reply(200, {});
mockAdapter.onGet(passwordStatusApiUrl).reply(200, {});
mockAdapter.onPost(licensesApiUrl, { user_email: testEmail }).reply(200, []);
mockAdapter.onGet(onboardingStatusApiUrl).reply(200, onboardingDefaultResponse);

const response = await api.getAllUserData(testUsername);
expect(response).toEqual({
errors: [],
user: { ...successDictResponse, passwordStatus: {} },
ssoRecords: [],
verificationStatus: { extraData: {} },
enrollments: {},
enrollments: [],
entitlements: { results: [], next: null },
licenses: { results: [], status: '' },
onboardingStatus: { ...onboardingDefaultResponse, onboardingStatus: 'No Paid Enrollment' },
});
});
});
Expand Down
8 changes: 8 additions & 0 deletions src/users/data/test/userSummary.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@ const UserSummaryData = {
isVerified: true,
extraData: [],
},
onboardingData: {
onboardingStatus: 'verified',
expirationDate: null,
onboardingLink: '/course/course-uuid/some-route',
onboardingPastDue: false,
onboardingReleaseDate: new Date().toISOString(),
reviewRequirementsUrl: null,
},
};

export default UserSummaryData;
4 changes: 4 additions & 0 deletions src/users/data/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,7 @@ export const getResetPasswordUrl = () => `${
export const getAccountActivationUrl = (activationKey) => `${
LMS_BASE_URL
}/activate/${activationKey}`;

export const getOnboardingStatusUrl = (courseId, username) => `${
LMS_BASE_URL
}/api/edx_proctoring/v1/user_onboarding/status?course_id=${encodeURIComponent(courseId)}&username=${encodeURIComponent(username)}`;
5 changes: 5 additions & 0 deletions src/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@ export function sort(firstElement, secondElement, key, direction) {
}
return 0;
}

/** Convert a string containing space and/or underscore (snake_case) into titleCase. e.g. hello_world -> Hello World */
export function titleCase(str) {
return str.toLowerCase().replace(/_/g, ' ').replace(/\b(\w)/g, s => s.toUpperCase());
}
22 changes: 21 additions & 1 deletion src/utils/index.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
isEmail, isValidUsername, formatDate, sort,
isEmail, isValidUsername, formatDate, sort, titleCase,
} from './index';

describe('Test Utils', () => {
Expand Down Expand Up @@ -70,4 +70,24 @@ describe('Test Utils', () => {
expect(sort(sortDict1, sortDict1, 'id', 'asc')).toEqual(0);
});
});

describe('titleCase', () => {
it('empty string', () => {
expect(titleCase('')).toEqual('');
expect(titleCase(' ')).toEqual(' ');
});
it('one word string', () => {
expect(titleCase('hello')).toEqual('Hello');
expect(titleCase('title')).toEqual('Title');
});
it('string with spaces', () => {
expect(titleCase('hello world')).toEqual('Hello World');
expect(titleCase('title case')).toEqual('Title Case');
});
it('string with underscore', () => {
expect(titleCase('hello_world')).toEqual('Hello World');
expect(titleCase('title_case')).toEqual('Title Case');
expect(titleCase('onboarding_exam_details')).toEqual('Onboarding Exam Details');
});
});
});

0 comments on commit e46dc50

Please sign in to comment.