Skip to content

Commit

Permalink
Merge pull request #273 from openedx/mfrank/learner-record-tab
Browse files Browse the repository at this point in the history
feat: learner record tab
  • Loading branch information
MaxFrank13 authored Oct 6, 2022
2 parents e4cd24e + 5078f2c commit 1cdc8c6
Show file tree
Hide file tree
Showing 14 changed files with 547 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ LOGIN_URL=null
LOGOUT_URL=null
MARKETING_SITE_BASE_URL=null
ORDER_HISTORY_URL=null
RECORDS_BASE_URL=null
REFRESH_ACCESS_TOKEN_ENDPOINT=null
SEGMENT_KEY=null
SITE_NAME=null
USE_LEARNER_RECORD_TAB=null
USER_INFO_COOKIE_NAME=null
PUBLISHER_BASE_URL=null
2 changes: 2 additions & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@ LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg/
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
RECORDS_BASE_URL='localhost:1990'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USE_LEARNER_RECORD_TAB='true'
USER_INFO_COOKIE_NAME='edx-user-info'
PUBLISHER_BASE_URL='http://localhost:18400'
2 changes: 2 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ LOGIN_URL='http://localhost:18000/login'
LOGOUT_URL='http://localhost:18000/logout'
MARKETING_SITE_BASE_URL='http://localhost:18000'
ORDER_HISTORY_URL='localhost:1996/orders'
RECORDS_BASE_URL='localhost:1990'
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
SEGMENT_KEY=null
SITE_NAME='edX'
USE_LEARNER_RECORD_TAB='true'
USER_INFO_COOKIE_NAME='edx-user-info'
2 changes: 2 additions & 0 deletions src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import './index.scss';

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

subscribe(APP_READY, () => {
Expand Down
2 changes: 2 additions & 0 deletions src/setupTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Enzyme.configure({ adapter: new Adapter() });

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

initialize({
Expand Down
10 changes: 9 additions & 1 deletion src/users/LearnerInformation.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Tabs, Tab } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform';
import UserSummary from './UserSummary';
import SingleSignOnRecords from './SingleSignOnRecords';
import Licenses from './licenses/Licenses';
import EntitlementsAndEnrollmentsContainer from './EntitlementsAndEnrollmentsContainer';
import LearnerCredentials from './LearnerCredentials';
import LearnerRecords from './LearnerRecords';

export default function LearnerInformation({
user, changeHandler,
}) {
const { USE_LEARNER_RECORD_TAB } = getConfig();
return (
<>
<br />
Expand Down Expand Up @@ -45,7 +48,12 @@ export default function LearnerInformation({
<br />
<LearnerCredentials username={user.username} />
</Tab>

{USE_LEARNER_RECORD_TAB && (
<Tab eventKey="records" title="Learner Records">
<br />
<LearnerRecords username={user.username} />
</Tab>
)}
</Tabs>
</>
);
Expand Down
27 changes: 26 additions & 1 deletion src/users/LearnerInformation.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ describe('Learners and Enrollments component', () => {

it('renders correctly', () => {
const tabs = wrapper.find('nav.nav-tabs a');
expect(tabs.length).toEqual(4);
expect(tabs.length).toEqual(5);

expect(tabs.at(0).text()).toEqual('Account Information');
expect(tabs.at(1).text()).toEqual('Enrollments/Entitlements');
expect(tabs.at(2).text()).toEqual('SSO/License Info');
expect(tabs.at(3).text()).toEqual('Learner Credentials');
expect(tabs.at(4).text()).toEqual('Learner Records');
});

it('Account Information Tab', () => {
Expand All @@ -71,6 +72,7 @@ describe('Learners and Enrollments component', () => {
expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(3).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(4).html()).not.toEqual(expect.stringContaining('active'));

const accountInfo = wrapper.find('.tab-content div#learner-information-tabpane-account');
expect(accountInfo.html()).toEqual(expect.stringContaining('active'));
Expand All @@ -86,6 +88,7 @@ describe('Learners and Enrollments component', () => {
expect(tabs.at(1).html()).toEqual(expect.stringContaining('active'));
expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(3).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(4).html()).not.toEqual(expect.stringContaining('active'));

const enrollmentsEntitlements = wrapper.find('.tab-content div#learner-information-tabpane-enrollments-entitlements');
expect(enrollmentsEntitlements.html()).toEqual(expect.stringContaining('active'));
Expand All @@ -102,6 +105,7 @@ describe('Learners and Enrollments component', () => {
expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(2).html()).toEqual(expect.stringContaining('active'));
expect(tabs.at(3).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(4).html()).not.toEqual(expect.stringContaining('active'));

const ssoRecords = wrapper.find('.tab-content div#learner-information-tabpane-sso');
expect(ssoRecords.html()).toEqual(expect.stringContaining('active'));
Expand All @@ -122,6 +126,7 @@ describe('Learners and Enrollments component', () => {
expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(3).html()).toEqual(expect.stringContaining('active'));
expect(tabs.at(4).html()).not.toEqual(expect.stringContaining('active'));

const credentials = wrapper.find(
'.tab-content div#learner-information-tabpane-credentials',
Expand All @@ -131,4 +136,24 @@ describe('Learners and Enrollments component', () => {
expect.stringContaining('Learner Credentials'),
);
});

it('Learner Records Tab', () => {
let tabs = wrapper.find('nav.nav-tabs a');

tabs.at(4).simulate('click');
tabs = wrapper.find('nav.nav-tabs a');
expect(tabs.at(0).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(1).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(2).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(3).html()).not.toEqual(expect.stringContaining('active'));
expect(tabs.at(4).html()).toEqual(expect.stringContaining('active'));

const records = wrapper.find(
'.tab-content div#learner-information-tabpane-records',
);
expect(records.html()).toEqual(expect.stringContaining('active'));
expect(records.html()).toEqual(
expect.stringContaining('Learner Records'),
);
});
});
152 changes: 152 additions & 0 deletions src/users/LearnerRecords.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { getConfig } from '@edx/frontend-platform';
import { Alert, Button } from '@edx/paragon';
import PropTypes from 'prop-types';
import React, { useEffect, useState } from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import Table from '../components/Table';
import { getLearnerRecords } from './data/api';

import messages from './messages';

function LearnerRecords({ username, intl }) {
const [records, setRecords] = useState(null);
const [error, setError] = useState(null);
const { RECORDS_BASE_URL } = getConfig();

useEffect(() => {
if (username) {
getLearnerRecords(username).then((data) => {
if (!data.errors) {
setRecords(data);
} else {
setError(data.errors[0]);
}
});
}
}, [username]);

const handleCopyButton = (uuid) => {
navigator.clipboard.writeText(`${RECORDS_BASE_URL}/shared/${uuid}`);
};

const renderStatus = (program) => {
if (program.completed) {
return (
intl.formatMessage(messages.earnedStatus)
);
}
if (program.empty) {
return (
intl.formatMessage(messages.notEarnedStatus)
);
}
return (
intl.formatMessage(messages.partiallyCompletedStatus)
);
};

const renderDate = (date) => (
`${intl.formatMessage(messages.recordTableLastUpdated)}: ${new Date(date).toLocaleDateString()}`
);

return (
<section>
<h3>{intl.formatMessage(messages.learnerRecordsTabHeader)}</h3>
{
records ? (
records.map(({ record, uuid }, idx) => (
<section key={uuid} className={`${idx % 2 ? 'bg-light-100' : 'bg-light-200'} p-4`}>
<div className="d-flex align-items-center justify-content-between">
<div>
<h4>{record.program.name}</h4>
<p>{record.program.type_name}</p>
<p>{renderStatus(record.program)}</p>
<p>{renderDate(record.program.last_updated)}</p>
</div>
{record.shared_program_record_uuid && (
<Button
variant="primary"
onClick={() => handleCopyButton(record.shared_program_record_uuid.replaceAll('-', ''))}
>
{intl.formatMessage(messages.copyPublicRecordLinkButton)}
</Button>
)}
</div>
<Table
columns={[
{
Header: intl.formatMessage(messages.recordTableHeaderCourseName),
accessor: 'name',
},
{
Header: intl.formatMessage(messages.recordTableHeaderSchool),
accessor: 'school',
},
{
Header: intl.formatMessage(messages.recordTableHeaderCourseId),
accessor: 'course_id',
},
{
Header: intl.formatMessage(messages.recordTableHeaderHighestGrade),
accessor: 'percent_grade',
},
{
Header: intl.formatMessage(messages.recordTableHeaderLetterGrade),
accessor: 'letter_grade',
},
{
Header: intl.formatMessage(messages.recordTableHeaderVerifiedAttempts),
accessor: 'attempts',
},
{
Header: intl.formatMessage(messages.recordTableHeaderDateEarned),
accessor: 'issue_date',
},
{
Header: intl.formatMessage(messages.recordTableHeaderStatus),
accessor: 'status',
},
]}
data={record.grades.map(grade => ({
name: grade.name,
school: grade.school,
course_id: grade.course_id.split(':')[1],
letter_grade: grade.letter_grade,
attempts: grade.attempts,
percent_grade:
grade.issue_date
? `${parseInt(Math.round(grade.percent_grade * 100), 10).toString()}%`
: '',
issue_date:
grade.issue_date
? new Date(grade.issue_date).toLocaleDateString()
: '',
status:
grade.issue_date
? intl.formatMessage(messages.earnedStatus)
: intl.formatMessage(messages.notEarnedStatus),
}))}
styleName="custom-table"
/>
</section>
))
) : (
<>
{error ? (
<Alert variant="danger">{error.text}</Alert>
) : (
<p>{`${intl.formatMessage(messages.noRecordsFound)}: ${username}`}</p>
)}
</>
)
}
</section>
);
}

LearnerRecords.propTypes = {
username: PropTypes.string.isRequired,
intl: intlShape.isRequired,
};

export default injectIntl(LearnerRecords);
Loading

0 comments on commit 1cdc8c6

Please sign in to comment.