Compare commits

..

13 Commits

Author SHA1 Message Date
edX requirements bot
4d7d95e490 feat: Add package-lock file version check 2022-04-29 08:51:09 -04:00
Matthew Carter
0a90024de9 feat: Update Demo Mode banner (#105)
* chore: Update MFE page title

* feat: Demo mode banner includes end date and call to action
2022-04-27 12:55:10 -04:00
edx-semantic-release
91d06e9788 chore(i18n): update translations 2022-04-24 11:45:23 -04:00
Leangseu Kim
74423bf359 feat: prevent download large files 2022-04-21 09:37:11 -04:00
leangseu-edx
7e9eab24b0 header component (#101)
* chore: use LearningHeader instead course header

* chore: remove course header debris
2022-04-20 13:13:03 -04:00
leangseu-edx
91dd10917f fix: cannot select criterion (#100)
* fix: cannot select criterion

* fix: refactor fill grade data

* fix: update tests

Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
2022-04-18 16:43:37 -04:00
edx-semantic-release
b2098be114 chore(i18n): update translations 2022-04-17 11:50:11 -04:00
leangseu-edx
64ac98c310 download filename, error handling and cache busting (#98)
* feat: handle download error and display them

* chore: update test environment for easier single file test

* feat: add cache bursting to the download
2022-04-14 13:52:39 -04:00
Leangseu Kim
8a80e2a70e chore: update package 2022-04-12 10:36:25 -04:00
Matthew Carter
a936d970db Merge pull request #95 from muselesscreator/batch_unlock_api
feat: Batch unlock api
2022-04-11 10:56:21 -04:00
Ben Warzeski
56c6c88638 feat: connect batch unlock to the api 2022-04-07 15:55:49 -04:00
Ben Warzeski
9c42bfbd8a fix: update snapshot 2022-04-07 15:53:09 -04:00
Ben Warzeski
69733f7837 fix: update routing for images 2022-04-07 15:52:50 -04:00
58 changed files with 17376 additions and 16738 deletions

View File

@@ -0,0 +1,13 @@
#check package-lock file version
name: Lockfile Version check
on:
push:
branches:
- master
pull_request:
jobs:
version-check:
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -14,4 +14,5 @@ module.exports = createConfig('jest', {
'src/postcss.config.js',
],
testTimeout: 120000,
testEnvironment: 'jsdom',
});

32628
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -28,7 +28,8 @@
"dependencies": {
"@edx/brand": "npm:@edx/brand-edx.org@^2.0.3",
"@edx/frontend-component-footer": "10.1.6",
"@edx/frontend-platform": "1.12.4",
"@edx/frontend-component-header": "^2.4.6",
"@edx/frontend-platform": "^1.15.6",
"@edx/paragon": "16.14.4",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
@@ -51,11 +52,10 @@
"history": "5.0.1",
"html-react-parser": "^1.3.0",
"lodash": "^4.17.21",
"node-sass": "^6.0.1",
"prop-types": "15.7.2",
"query-string": "7.0.1",
"react": "17.0.2",
"react-dom": "17.0.2",
"react": "^16.14.0",
"react-dom": "^16.14.0",
"react-intl": "^5.20.9",
"react-pdf": "^5.5.0",
"react-redux": "^7.2.4",
@@ -73,7 +73,7 @@
"whatwg-fetch": "^3.6.2"
},
"devDependencies": {
"@edx/frontend-build": "9.1.1",
"@edx/frontend-build": "^9.1.4",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.1.0",
"axios-mock-adapter": "^1.20.0",
@@ -86,7 +86,7 @@
"jest": "27.0.6",
"jest-expect-message": "^1.0.2",
"react-dev-utils": "^11.0.4",
"react-test-renderer": "^17.0.2",
"react-test-renderer": "^16.14.0",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.4",
"semantic-release": "^17.4.5"

View File

@@ -1,7 +1,7 @@
<!doctype html>
<html lang="en-us" dir="ltr">
<head>
<title>ORA Enhanced Staff Grader | <%= process.env.SITE_NAME %></title>
<title>ORA staff grading | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />

View File

@@ -4,11 +4,11 @@ import { connect } from 'react-redux';
import { BrowserRouter as Router } from 'react-router-dom';
import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import { selectors } from 'data/redux';
import DemoWarning from 'containers/DemoWarning';
import CourseHeader from 'containers/CourseHeader';
import ListView from 'containers/ListView';
import './App.scss';
@@ -16,7 +16,7 @@ import './App.scss';
export const App = ({ courseMetadata, isEnabled }) => (
<Router>
<div>
<CourseHeader
<Header
courseTitle={courseMetadata.title}
courseNumber={courseMetadata.number}
courseOrg={courseMetadata.org}

View File

@@ -42,32 +42,6 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
}
}
.course-header {
min-width: 0;
border-bottom: 1px solid black;
.course-title-lockup {
min-width: 0;
span {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding-bottom: 0.1rem;
}
}
.user-dropdown {
.btn {
height: 3rem;
@media (max-width: -1 + map-get($grid-breakpoints, "sm")) {
padding: 0 0.5rem;
}
}
}
}
#paragon-portal-root {
.pgn__modal-layer {
.pgn__modal-close-container {

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { shallow } from 'enzyme';
import Footer from '@edx/frontend-component-footer';
import { LearningHeader as Header } from '@edx/frontend-component-header';
import ListView from 'containers/ListView';
@@ -16,11 +17,13 @@ jest.mock('data/redux', () => ({
},
}));
jest.mock('@edx/frontend-component-header', () => ({
LearningHeader: 'Header',
}));
jest.mock('@edx/frontend-component-footer', () => 'Footer');
jest.mock('containers/DemoWarning', () => 'DemoWarning');
jest.mock('containers/ListView', () => 'ListView');
jest.mock('containers/CourseHeader', () => 'CourseHeader');
const logo = 'fakeLogo.png';
let el;
@@ -57,5 +60,16 @@ describe('App router component', () => {
test('Footer logo drawn from env variable', () => {
expect(router.find(Footer).props().logo).toEqual(logo);
});
test('Header to use courseMetadata props', () => {
const {
courseTitle,
courseNumber,
courseOrg,
} = router.find(Header).props();
expect(courseTitle).toEqual(props.courseMetadata.title);
expect(courseNumber).toEqual(props.courseMetadata.number);
expect(courseOrg).toEqual(props.courseMetadata.org);
});
});
});

View File

@@ -3,7 +3,7 @@
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
<BrowserRouter>
<div>
<CourseHeader
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
@@ -22,7 +22,7 @@ exports[`App router component snapshot: disabled (show demo warning) 1`] = `
exports[`App router component snapshot: enabled 1`] = `
<BrowserRouter>
<div>
<CourseHeader
<Header
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"

View File

@@ -6,7 +6,7 @@ import {
Icon, Form, ActionRow, IconButton,
} from '@edx/paragon';
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
import pdfjsWorker from 'react-pdf/node_modules/pdfjs-dist/build/pdf.worker.entry';
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import message from './messages';
export const getRegisterUrl = () => {
const { LMS_BASE_URL } = getConfig();
const locationHref = encodeURIComponent(global.location.href);
return `${LMS_BASE_URL}/register?next=${locationHref}`;
};
export const AnonymousUserMenu = ({ intl }) => (
<div>
<Button
className="mr-3"
variant="outline-primary"
href={getRegisterUrl()}
>
{intl.formatMessage(message.registerSentenceCase)}
</Button>
<Button
variant="primary"
href={`${getLoginRedirectUrl(global.location.href)}`}
>
{intl.formatMessage(message.signInSentenceCase)}
</Button>
</div>
);
AnonymousUserMenu.propTypes = {
intl: intlShape.isRequired,
};
export default injectIntl(AnonymousUserMenu);

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AnonymousUserMenu } from './AnonymousUserMenu';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
}),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getLoginRedirectUrl: (url) => `redirect:${url}`,
}));
describe('Header AnonymousUserMenu component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
};
test('snapshot', () => {
expect(
shallow(<AnonymousUserMenu {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -1,27 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faUserCircle } from '@fortawesome/free-solid-svg-icons';
import { Dropdown } from '@edx/paragon';
export const UserAvatar = ({ username }) => (
<Dropdown.Toggle variant="outline-primary">
<FontAwesomeIcon
icon={faUserCircle}
className="d-md-none"
size="lg"
/>
<span data-hj-suppress className="d-none d-md-inline">
{username}
</span>
</Dropdown.Toggle>
);
UserAvatar.propTypes = {
username: PropTypes.string.isRequired,
};
UserAvatar.defaultProps = {};
export default UserAvatar;

View File

@@ -1,23 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import UserAvatar from './UserAvatar';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
LOGOUT_URL: '<LOGOUT_URL>',
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
describe('Header AuthenticatedUserDropdown UserAvatar component', () => {
const props = {
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<UserAvatar {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -1,40 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import messages from '../messages';
export class UserMenu extends React.Component {
menuItem(href, message) {
return (
<Dropdown.Item href={href}>
{this.props.intl.formatMessage(message)}
</Dropdown.Item>
);
}
render() {
const { username } = this.props;
const { LMS_BASE_URL, LOGOUT_URL } = getConfig();
return (
<Dropdown.Menu className="dropdown-menu-right">
{this.menuItem(`${LMS_BASE_URL}/dashboard`, messages.dashboard)}
{this.menuItem(`${LMS_BASE_URL}/u/${username}`, messages.profile)}
{this.menuItem(`${LMS_BASE_URL}/account/settings`, messages.account)}
{this.menuItem(LOGOUT_URL, messages.signOut)}
</Dropdown.Menu>
);
}
}
UserMenu.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
};
UserMenu.defaultProps = {};
export default injectIntl(UserMenu);

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { UserMenu } from './UserMenu';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<LMS_BASE_URL>',
LOGOUT_URL: '<LOGOUT_URL>',
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
describe('Header AuthenticatedUserDropdown UserMenu component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<UserMenu {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -1,31 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown UserAvatar component snapshot 1`] = `
<Dropdown.Toggle
variant="outline-primary"
>
<FontAwesomeIcon
className="d-md-none"
icon={
Object {
"icon": Array [
496,
512,
Array [],
"f2bd",
"M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zm0 96c48.6 0 88 39.4 88 88s-39.4 88-88 88-88-39.4-88-88 39.4-88 88-88zm0 344c-58.7 0-111.3-26.6-146.5-68.2 18.8-35.4 55.6-59.8 98.5-59.8 2.4 0 4.8.4 7.1 1.1 13 4.2 26.6 6.9 40.9 6.9 14.3 0 28-2.7 40.9-6.9 2.3-.7 4.7-1.1 7.1-1.1 42.9 0 79.7 24.4 98.5 59.8C359.3 421.4 306.7 448 248 448z",
],
"iconName": "user-circle",
"prefix": "fas",
}
}
size="lg"
/>
<span
className="d-none d-md-inline"
data-hj-suppress={true}
>
test-username
</span>
</Dropdown.Toggle>
`;

View File

@@ -1,28 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown UserMenu component snapshot 1`] = `
<Dropdown.Menu
className="dropdown-menu-right"
>
<Dropdown.Item
href="<LMS_BASE_URL>/dashboard"
>
Dashboard
</Dropdown.Item>
<Dropdown.Item
href="<LMS_BASE_URL>/u/test-username"
>
Profile
</Dropdown.Item>
<Dropdown.Item
href="<LMS_BASE_URL>/account/settings"
>
Account
</Dropdown.Item>
<Dropdown.Item
href="<LOGOUT_URL>"
>
Sign Out
</Dropdown.Item>
</Dropdown.Menu>
`;

View File

@@ -1,22 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AuthenticatedUserDropdown component snapshot 1`] = `
<Fragment>
<a
className="text-gray-700 mr-3"
href="<SUPPORT_URL>"
>
Help
</a>
<Dropdown
className="user-dropdown"
>
<UserAvatar
username="test-username"
/>
<UserMenu
username="test-username"
/>
</Dropdown>
</Fragment>
`;

View File

@@ -1,35 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Dropdown } from '@edx/paragon';
import UserMenu from './UserMenu';
import UserAvatar from './UserAvatar';
import messages from '../messages';
export const AuthenticatedUserDropdown = ({
intl,
username,
}) => (
<>
<a className="text-gray-700 mr-3" href={`${getConfig().SUPPORT_URL}`}>
{intl.formatMessage(messages.help)}
</a>
<Dropdown className="user-dropdown">
<UserAvatar username={username} />
<UserMenu username={username} />
</Dropdown>
</>
);
AuthenticatedUserDropdown.propTypes = {
intl: intlShape.isRequired,
username: PropTypes.string.isRequired,
};
AuthenticatedUserDropdown.defaultProps = {};
export default injectIntl(AuthenticatedUserDropdown);

View File

@@ -1,24 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AuthenticatedUserDropdown } from '.';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
SUPPORT_URL: '<SUPPORT_URL>',
}),
}));
jest.mock('./UserAvatar', () => 'UserAvatar');
jest.mock('./UserMenu', () => 'UserMenu');
describe('Header AuthenticatedUserDropdown component', () => {
const props = {
intl: { formatMessage: (msg) => msg.defaultMessage },
username: 'test-username',
};
test('snapshot', () => {
expect(
shallow(<AuthenticatedUserDropdown {...props} />),
).toMatchSnapshot();
});
});

View File

@@ -1,32 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
export const CourseLabel = ({
courseOrg,
courseNumber,
courseTitle,
}) => (
<div
className="flex-grow-1 course-title-lockup"
style={{ lineHeight: 1 }}
>
<span className="d-block small m-0">
{courseOrg} {courseNumber}
</span>
<span className="d-block m-0 font-weight-bold course-title">
{courseTitle}
</span>
</div>
);
CourseLabel.propTypes = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
};
CourseLabel.defaultProps = {
courseOrg: null,
courseNumber: null,
courseTitle: null,
};
export default CourseLabel;

View File

@@ -1,18 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import CourseLabel from './CourseLabel';
const courseData = {
courseOrg: 'course-org',
courseNumber: 'course-number',
courseTitle: 'course-title',
};
describe('Header CourseLabel component', () => {
test('snapshot', () => {
expect(
shallow(<CourseLabel {...courseData} />),
).toMatchSnapshot();
});
});

View File

@@ -1,17 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
const LinkedLogo = () => (
<a
className="logo"
href={`${getConfig().LMS_BASE_URL}/dashboard`}
>
<img
className="d-block"
src={getConfig().LOGO_URL}
alt={getConfig().SITE_NAME}
/>
</a>
);
export default LinkedLogo;

View File

@@ -1,20 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import LinkedLogo from './LinkedLogo';
jest.mock('@edx/frontend-platform', () => ({
getConfig: () => ({
LMS_BASE_URL: '<getConfig().LMS_BASE_URL>',
LOGO_URL: '<getConfig().LOGO_URL>',
SITE_NAME: '<getConfig().SITE_NAME>',
}),
}));
describe('Header CourseLabel component', () => {
test('snapshot', () => {
expect(
shallow(<LinkedLogo />),
).toMatchSnapshot();
});
});

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header AnonymousUserMenu component snapshot 1`] = `
<div>
<Button
className="mr-3"
href="<LMS_BASE_URL>/register?next=http%3A%2F%2Flocalhost%2F"
variant="outline-primary"
>
Register
</Button>
<Button
href="redirect:http://localhost/"
variant="primary"
>
Sign in
</Button>
</div>
`;

View File

@@ -1,25 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header CourseLabel component snapshot 1`] = `
<div
className="flex-grow-1 course-title-lockup"
style={
Object {
"lineHeight": 1,
}
}
>
<span
className="d-block small m-0"
>
course-org
course-number
</span>
<span
className="d-block m-0 font-weight-bold course-title"
>
course-title
</span>
</div>
`;

View File

@@ -1,14 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header CourseLabel component snapshot 1`] = `
<a
className="logo"
href="<getConfig().LMS_BASE_URL>/dashboard"
>
<img
alt="<getConfig().SITE_NAME>"
className="d-block"
src="<getConfig().LOGO_URL>"
/>
</a>
`;

View File

@@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header component snapshot 1`] = `
<header
className="course-header"
>
<a
className="sr-only sr-only-focusable"
href="#main-content"
>
Skip to main content.
</a>
<div
className="container-xl py-2 d-flex align-items-center"
>
<LinkedLogo />
<CourseLabel
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<AnonymousUserMenu />
</div>
</header>
`;
exports[`Header component snapshot with authenticatedUser 1`] = `
<header
className="course-header"
>
<a
className="sr-only sr-only-focusable"
href="#main-content"
>
Skip to main content.
</a>
<div
className="container-xl py-2 d-flex align-items-center"
>
<LinkedLogo />
<CourseLabel
courseNumber="course-number"
courseOrg="course-org"
courseTitle="course-title"
/>
<AuthenticatedUserDropdown
username="test"
/>
</div>
</header>
`;

View File

@@ -1,47 +0,0 @@
import React, { useContext } from 'react';
import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import AnonymousUserMenu from './AnonymousUserMenu';
import AuthenticatedUserDropdown from './AuthenticatedUserDropdown';
import LinkedLogo from './LinkedLogo';
import CourseLabel from './CourseLabel';
import messages from './messages';
export const Header = ({
courseOrg,
courseNumber,
courseTitle,
intl,
}) => {
const { authenticatedUser } = useContext(AppContext);
return (
<header className="course-header">
<a className="sr-only sr-only-focusable" href="#main-content">
{intl.formatMessage(messages.skipNavLink)}
</a>
<div className="container-xl py-2 d-flex align-items-center">
<LinkedLogo />
<CourseLabel {...{ courseOrg, courseNumber, courseTitle }} />
{authenticatedUser
? (<AuthenticatedUserDropdown username={authenticatedUser.username} />)
: (<AnonymousUserMenu />)}
</div>
</header>
);
};
Header.propTypes = {
courseOrg: PropTypes.string,
courseNumber: PropTypes.string,
courseTitle: PropTypes.string,
intl: intlShape.isRequired,
};
Header.defaultProps = {
courseOrg: null,
courseNumber: null,
courseTitle: null,
};
export default injectIntl(Header);

View File

@@ -1,38 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { AppContext } from '@edx/frontend-platform/react';
import { Header } from '.';
jest.mock('./AnonymousUserMenu', () => 'AnonymousUserMenu');
jest.mock('./AuthenticatedUserDropdown', () => 'AuthenticatedUserDropdown');
jest.mock('./LinkedLogo', () => 'LinkedLogo');
jest.mock('./CourseLabel', () => 'CourseLabel');
jest.mock('@edx/frontend-platform/react', () => ({
AppContext: { authenticatedUser: null },
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: (context) => context,
}));
const courseData = {
courseOrg: 'course-org',
courseNumber: 'course-number',
courseTitle: 'course-title',
};
describe('Header component', () => {
const props = {
...courseData,
intl: { formatMessage: (msg) => msg.defaultMessage },
};
test('snapshot', () => {
expect(shallow(<Header {...props} />)).toMatchSnapshot();
});
test('snapshot with authenticatedUser', () => {
AppContext.authenticatedUser = { username: 'test' };
expect(shallow(<Header {...props} />)).toMatchSnapshot();
});
});

View File

@@ -1,56 +0,0 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
courseMaterial: {
id: 'learn.navigation.course.tabs.label',
defaultMessage: 'Course Material',
description: 'The accessible label for course tabs navigation',
},
dashboard: {
id: 'header.menu.dashboard.label',
defaultMessage: 'Dashboard',
description: 'The text for the user menu Dashboard navigation link.',
},
help: {
id: 'header.help.label',
defaultMessage: 'Help',
description: 'The text for the link to the Help Center',
},
profile: {
id: 'header.menu.profile.label',
defaultMessage: 'Profile',
description: 'The text for the user menu Profile navigation link.',
},
account: {
id: 'header.menu.account.label',
defaultMessage: 'Account',
description: 'The text for the user menu Account navigation link.',
},
orderHistory: {
id: 'header.menu.orderHistory.label',
defaultMessage: 'Order History',
description: 'The text for the user menu Order History navigation link.',
},
skipNavLink: {
id: 'header.navigation.skipNavLink',
defaultMessage: 'Skip to main content.',
description: 'A link used by screen readers to allow users to skip to the main content of the page.',
},
signOut: {
id: 'header.menu.signOut.label',
defaultMessage: 'Sign Out',
description: 'The label for the user menu Sign Out action.',
},
registerSentenceCase: {
id: 'header.register.sentenceCase',
defaultMessage: 'Register',
description: 'Text in a button, prompting the user to register.',
},
signInSentenceCase: {
id: 'header.signIn.sentenceCase',
defaultMessage: 'Sign in',
description: 'Text in a button, prompting the user to log in.',
},
});
export default messages;

View File

@@ -16,7 +16,7 @@ exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`]
</Alert.Heading>
<p>
<FormattedMessage
defaultMessage="You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature."
defaultMessage="You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support."
description="Demo mode message"
id="ora-grading.ReviewModal.demoMessage"
/>

View File

@@ -10,7 +10,7 @@ const messages = defineMessages({
},
demoModeMessage: {
id: 'ora-grading.ReviewModal.demoMessage',
defaultMessage: 'You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature.',
defaultMessage: 'You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support.',
description: 'Demo mode message',
},
});

View File

@@ -2,11 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import {
Card, Collapsible, Icon, DataTable,
Card, Collapsible, Icon, DataTable, Button,
} from '@edx/paragon';
import { ArrowDropDown, ArrowDropUp } from '@edx/paragon/icons';
import { ArrowDropDown, ArrowDropUp, WarningFilled } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { downloadAllLimit, downloadSingleLimit } from 'data/constants/files';
import FileNameCell from './components/FileNameCell';
import FileExtensionCell from './components/FileExtensionCell';
import FilePopoverCell from './components/FilePopoverCell';
@@ -19,7 +21,17 @@ import messages from './messages';
*/
export class SubmissionFiles extends React.Component {
get title() {
return `Submission Files (${this.props.files.length})`;
return `${this.props.intl.formatMessage(messages.submissionFiles)} (${this.props.files.length})`;
}
get canDownload() {
let totalFileSize = 0;
const exceedFileSize = this.props.files.some(file => {
totalFileSize += file.size;
return file.size > downloadSingleLimit;
});
return !exceedFileSize && totalFileSize < downloadAllLimit;
}
render() {
@@ -70,7 +82,15 @@ export class SubmissionFiles extends React.Component {
</Collapsible.Body>
</Collapsible.Advanced>
<Card.Footer className="text-right">
<FileDownload files={files} />
{
this.canDownload ? <FileDownload files={files} /> : (
<div>
<Icon className="d-inline-block align-middle" src={WarningFilled} />
<span className="exceed-download-text"> {intl.formatMessage(messages.exceedFileSize)} </span>
<Button disabled>{intl.formatMessage(messages.downloadFiles)}</Button>
</div>
)
}
</Card.Footer>
</>
) : (

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { shallow } from 'enzyme';
import { downloadAllLimit, downloadSingleLimit } from 'data/constants/files';
import { formatMessage } from 'testUtils';
import { SubmissionFiles } from './SubmissionFiles';
import messages from './messages';
jest.mock('./components/FileNameCell', () => jest.fn().mockName('FileNameCell'));
jest.mock('./components/FileExtensionCell', () => jest.fn().mockName('FileExtensionCell'));
@@ -16,25 +19,34 @@ describe('SubmissionFiles', () => {
name: 'some file name.jpg',
description: 'description for the file',
downloadURL: '/valid-url-wink-wink',
size: 0,
},
{
name: 'file number 2.jpg',
description: 'description for this file',
downloadURL: '/url-2',
size: 0,
},
],
};
let el;
beforeAll(() => {
el = shallow(<SubmissionFiles intl={{ formatMessage }} />);
beforeEach(() => {
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} />);
});
describe('snapshot', () => {
test('files does not exist', () => {
test('files existed for props', () => {
expect(el).toMatchSnapshot();
});
test('files exited for props', () => {
el.setProps({ ...props });
test('files does not exist', () => {
el.setProps({ files: [] });
expect(el).toMatchSnapshot();
});
test('files size exceed', () => {
const files = props.files.map(file => ({ ...file, size: downloadSingleLimit + 1 }));
el.setProps({ files });
expect(el).toMatchSnapshot();
});
});
@@ -43,12 +55,47 @@ describe('SubmissionFiles', () => {
test('title', () => {
const titleEl = el.find('.submission-files-title>h3');
expect(titleEl.text()).toEqual(
`Submission Files (${props.files.length})`,
`${formatMessage(messages.submissionFiles)} (${props.files.length})`,
);
expect(el.instance().title).toEqual(
`Submission Files (${props.files.length})`,
`${formatMessage(messages.submissionFiles)} (${props.files.length})`,
);
});
describe('canDownload', () => {
test('normal file size', () => {
expect(el.instance().canDownload).toEqual(true);
});
test('one of the file exceed the limit', () => {
const oneFileExceed = [{ ...props.files[0], size: downloadSingleLimit + 1 }, props.files[1]];
oneFileExceed.forEach(file => expect(file.size < downloadAllLimit).toEqual(true));
el.setProps({ files: oneFileExceed });
expect(el.instance().canDownload).toEqual(false);
const warningEl = el.find('span.exceed-download-text');
expect(warningEl.text().trim()).toEqual(formatMessage(messages.exceedFileSize));
});
test('total file size exceed the limit', () => {
const length = 20;
const totalFilesExceed = new Array(length).fill({
name: 'some file name.jpg',
description: 'description for the file',
downloadURL: '/valid-url-wink-wink',
size: (downloadAllLimit + 1) / length,
});
totalFilesExceed.forEach(file => {
expect(file.size < downloadAllLimit).toEqual(true);
expect(file.size < downloadSingleLimit).toEqual(true);
});
el.setProps({ files: totalFilesExceed });
expect(el.instance().canDownload).toEqual(false);
});
});
});
});
});

View File

@@ -14,7 +14,7 @@ exports[`SubmissionFiles component snapshot files does not exist 1`] = `
</Card>
`;
exports[`SubmissionFiles component snapshot files exited for props 1`] = `
exports[`SubmissionFiles component snapshot files existed for props 1`] = `
<Card
className="submission-files"
>
@@ -75,11 +75,13 @@ exports[`SubmissionFiles component snapshot files exited for props 1`] = `
"description": "description for the file",
"downloadURL": "/valid-url-wink-wink",
"name": "some file name.jpg",
"size": 0,
},
Object {
"description": "description for this file",
"downloadURL": "/url-2",
"name": "file number 2.jpg",
"size": 0,
},
]
}
@@ -100,11 +102,13 @@ exports[`SubmissionFiles component snapshot files exited for props 1`] = `
"description": "description for the file",
"downloadURL": "/valid-url-wink-wink",
"name": "some file name.jpg",
"size": 0,
},
Object {
"description": "description for this file",
"downloadURL": "/url-2",
"name": "file number 2.jpg",
"size": 0,
},
]
}
@@ -112,3 +116,105 @@ exports[`SubmissionFiles component snapshot files exited for props 1`] = `
</Card.Footer>
</Card>
`;
exports[`SubmissionFiles component snapshot files size exceed 1`] = `
<Card
className="submission-files"
>
<Collapsible.Advanced
defaultOpen={true}
>
<Collapsible.Trigger
className="submission-files-title"
>
<h3>
Submission Files (2)
</h3>
<Collapsible.Visible
whenClosed={true}
>
<Icon
src={[MockFunction icons.ArrowDropDown]}
/>
</Collapsible.Visible>
<Collapsible.Visible
whenOpen={true}
>
<Icon
src={[MockFunction icons.ArrowDropUp]}
/>
</Collapsible.Visible>
</Collapsible.Trigger>
<Collapsible.Body
className="submission-files-body"
>
<div
className="submission-files-table"
>
<DataTable
columns={
Array [
Object {
"Cell": [MockFunction FileNameCell],
"Header": "Name",
"accessor": "name",
},
Object {
"Cell": [MockFunction FileExtensionCell],
"Header": "File Extension",
"accessor": "name",
"id": "extension",
},
Object {
"Cell": [MockFunction FilePopoverCell],
"Header": "File Metadata",
"accessor": "",
},
]
}
data={
Array [
Object {
"description": "description for the file",
"downloadURL": "/valid-url-wink-wink",
"name": "some file name.jpg",
"size": 1610612737,
},
Object {
"description": "description for this file",
"downloadURL": "/url-2",
"name": "file number 2.jpg",
"size": 1610612737,
},
]
}
itemCount={2}
>
<DataTable.Table />
</DataTable>
</div>
</Collapsible.Body>
</Collapsible.Advanced>
<Card.Footer
className="text-right"
>
<div>
<Icon
className="d-inline-block align-middle"
/>
<span
className="exceed-download-text"
>
Exceeded the allow download size
</span>
<Button
disabled={true}
>
Download files
</Button>
</div>
</Card.Footer>
</Card>
`;

View File

@@ -36,6 +36,16 @@ const messages = defineMessages({
defaultMessage: 'Retry download',
description: 'Download files failed state label',
},
submissionFiles: {
id: 'ora-grading.ResponseDisplay.SubmissionFiles.submissionFile',
defaultMessage: 'Submission Files',
description: 'Total submission files',
},
exceedFileSize: {
id: 'ora-grading.ResponseDisplay.SubmissionFiles.fileSizeExceed',
defaultMessage: 'Exceeded the allow download size',
description: 'Exceed the allow download size error message',
},
});
export default messages;

View File

@@ -28,7 +28,7 @@ export class DownloadErrors extends React.Component {
if (!this.props.isFailed) { return null; }
return (
<ReviewError
key="lockFailed"
key="downloadFailed"
headingMessage={messages.downloadFailedHeading}
actions={{
cancel: { onClick: this.cancelAction, message: messages.dismiss },
@@ -36,19 +36,36 @@ export class DownloadErrors extends React.Component {
}}
>
<FormattedMessage {...messages.downloadFailedContent} />
<br />
<FormattedMessage {...messages.failedFiles} />
<ul>
{this.props.error.files.map(filename => (
<li key={filename}>{filename}</li>
))}
</ul>
</ReviewError>
);
}
}
DownloadErrors.defaultProps = {
error: {
files: [],
},
};
DownloadErrors.propTypes = {
// redux
clearState: PropTypes.func.isRequired,
isFailed: PropTypes.bool.isRequired,
error: PropTypes.shape({
files: PropTypes.arrayOf(PropTypes.string),
}),
downloadFiles: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
isFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.downloadFiles }),
error: selectors.requests.error(state, { requestKey: RequestKeys.downloadFiles }),
});
export const mapDispatchToProps = {

View File

@@ -14,7 +14,10 @@ let el;
jest.mock('data/redux', () => ({
selectors: {
requests: { isFailed: (...args) => ({ isFailed: args }) },
requests: {
isFailed: (...args) => ({ isFailed: args }),
error: (...args) => ({ error: args }),
},
},
actions: {
requests: { clearRequest: jest.fn() },
@@ -28,6 +31,9 @@ jest.mock('./ReviewError', () => 'ReviewError');
describe('DownloadErrors component', () => {
const props = {
isFailed: false,
error: {
files: [],
},
};
describe('component', () => {
beforeEach(() => {
@@ -40,7 +46,12 @@ describe('DownloadErrors component', () => {
el.instance().cancelAction = jest.fn().mockName('this.cancelAction');
});
test('failed: show error', () => {
el.setProps({ isFailed: true });
el.setProps({
isFailed: true,
error: {
files: ['file-1-failed.error', 'file-2.failed'],
},
});
expect(el.instance().render()).toMatchSnapshot();
expect(el.isEmptyRender()).toEqual(false);
});
@@ -68,6 +79,10 @@ describe('DownloadErrors component', () => {
const requestKey = RequestKeys.downloadFiles;
expect(mapped.isFailed).toEqual(selectors.requests.isFailed(testState, { requestKey }));
});
test('error loads from requests.error(downloadFiles)', () => {
const requestKey = RequestKeys.downloadFiles;
expect(mapped.error).toEqual(selectors.requests.error(testState, { requestKey }));
});
});
describe('mapDispatchToProps', () => {
it('loads clearState from actions.requests.clearRequest', () => {

View File

@@ -34,6 +34,20 @@ exports[`DownloadErrors component component snapshots failed: show error 1`] = `
description="Failed download error content"
id="ora-grading.ReviewModal.errorDownloadFailedContent"
/>
<br />
<FormattedMessage
defaultMessage="Failed files:"
description="List header for file download failure alert"
id="ora-grading.ReviewModal.errorDownloadFailedFiles"
/>
<ul>
<li>
file-1-failed.error
</li>
<li>
file-2.failed
</li>
</ul>
</ReviewError>
`;

View File

@@ -82,6 +82,11 @@ const messages = defineMessages({
defaultMessage: 'Retry download',
description: 'Failed download retry button text',
},
failedFiles: {
id: 'ora-grading.ReviewModal.errorDownloadFailedFiles',
defaultMessage: 'Failed files:',
description: 'List header for file download failure alert',
},
});
export default StrictDict(messages);

View File

@@ -14,4 +14,7 @@ export const FileTypes = StrictDict({
svg: 'svg',
});
export const downloadSingleLimit = 1610612736; // 1.5GB
export const downloadAllLimit = 10737418240; // 10GB
export default FileTypes;

View File

@@ -124,44 +124,46 @@ rubric.criteriaIndices = createSelector(
* Returns true iff the passed feedback value is required or optional
* @return {bool} - should include feedback?
*/
const shouldIncludeFeedback = (feedback) => ([
export const shouldIncludeFeedback = (feedback) => [
feedbackRequirement.required,
feedbackRequirement.optional,
]).includes(feedback);
].includes(feedback);
/**
* Returns an empty grade data object based on the rubric config loaded in the app model.
* @return {obj} - empty grade data object
* take current grade and fill the empty fill with default value
* @param {obj} gradeData
* @returns
*/
export const emptyGrade = createSelector(
[module.rubric.hasConfig, module.rubric.criteria, module.rubric.feedbackConfig],
(hasConfig, criteria, feedbackConfig) => {
if (!hasConfig) {
return null;
}
const gradeData = {};
if (shouldIncludeFeedback(feedbackConfig)) {
gradeData.overallFeedback = '';
}
gradeData.criteria = criteria.map(criterion => {
const entry = {
orderNum: criterion.orderNum,
name: criterion.name,
selectedOption: '',
};
if (shouldIncludeFeedback(criterion.feedback)) {
entry.feedback = '';
}
return entry;
});
return gradeData;
},
);
export const fillGradeData = (state, data) => {
const hasConfig = module.rubric.hasConfig(state);
if (!hasConfig || Array.isArray(data?.criteria)) {
return data;
}
const feedbackConfig = module.rubric.feedbackConfig(state);
const criteria = module.rubric.criteria(state);
const overallFeedback = (
module.shouldIncludeFeedback(feedbackConfig) && { overallFeedback: '' }
);
const criteriaFeedback = (feedback) => (
module.shouldIncludeFeedback(feedback) && { feedback: '' }
);
const gradeData = { ...overallFeedback };
gradeData.criteria = criteria.map(({ feedback, name, orderNum }) => ({
...criteriaFeedback(feedback),
name,
orderNum,
selectedOption: '',
}));
return gradeData;
};
export default StrictDict({
...simpleSelectors,
courseId,
ora,
rubric: StrictDict(rubric),
emptyGrade,
fillGradeData,
});

View File

@@ -1,5 +1,6 @@
import { feedbackRequirement } from 'data/services/lms/constants';
import { keyStore } from '../../../utils';
// import * in order to mock in-file references
import * as selectors from './selectors';
@@ -44,6 +45,8 @@ const testState = {
},
};
const selectorKeys = keyStore(selectors);
describe('app selectors unit tests', () => {
const { appSelector, simpleSelectors, rubric } = selectors;
describe('appSelector', () => {
@@ -180,56 +183,86 @@ describe('app selectors unit tests', () => {
});
});
});
describe('emptyGrade selector', () => {
const { rubricConfig } = testState.app.oraMetadata;
let preSelectors;
let cb;
describe('shouldIncludeFeedback', () => {
it('returns true iff the passed feedback is optional or required', () => {
expect(selectors.shouldIncludeFeedback(feedbackRequirement.optional)).toEqual(true);
expect(selectors.shouldIncludeFeedback(feedbackRequirement.required)).toEqual(true);
expect(selectors.shouldIncludeFeedback(feedbackRequirement.disabled)).toEqual(false);
expect(selectors.shouldIncludeFeedback('aribitrary')).toEqual(false);
});
});
describe('fillGradeData selector', () => {
const cb = selectors.fillGradeData;
const spies = {};
let oldRubric;
const criteria = [
{ name: 'criteria1', orderNum: 0, feedback: true },
{ name: 'criteria2', orderNum: 1, feedback: false },
{ name: 'criteria3', orderNum: 2, feedback: true },
];
const data = { arbitrary: 'data', criteria };
beforeAll(() => {
oldRubric = { ...rubric };
});
beforeEach(() => {
({ preSelectors, cb } = selectors.emptyGrade);
rubric.hasConfig = jest.fn(() => true);
rubric.feedbackConfig = jest.fn(() => true);
rubric.criteria = jest.fn(() => criteria);
spies.shouldIncludeFeedback = jest.spyOn(
selectors,
selectorKeys.shouldIncludeFeedback,
).mockImplementation(val => val);
});
it('is a memoized selector based on rubric.[hasConfig, criteria, feedbackConfig]', () => {
expect(preSelectors).toEqual([
rubric.hasConfig,
rubric.criteria,
rubric.feedbackConfig,
]);
afterEach(() => {
spies[selectorKeys.shouldIncludeFeedback].mockRestore();
});
describe('If the config is not loaded (hasConfig = undefined)', () => {
it('returns null', () => {
expect(cb(false, {}, '')).toEqual(null);
afterAll(() => {
selectors.rubric = { ...oldRubric };
});
describe('if rubric config is not loaded', () => {
it('returns passed gradeData', () => {
rubric.hasConfig.mockReturnValueOnce(false);
expect(cb(testState, data)).toEqual(data);
});
});
describe('The generated object', () => {
it('loads an overallFeedback field iff feedbackConfig is optional or required', () => {
let gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.optional);
expect(gradeData.overallFeedback).toEqual('');
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.required);
expect(gradeData.overallFeedback).toEqual('');
gradeData = cb(true, rubricConfig.criteria, feedbackRequirement.disabled);
expect(gradeData.overallFeedback).toEqual(undefined);
describe('if rubric config is loaded', () => {
describe('gradeData is passed, contains criteria', () => {
it('returns the passed gradeData', () => {
expect(cb(testState, data)).toEqual(data);
});
});
it('loads criteria with feedback field based on requirement config', () => {
const gradeData = cb(true, rubricConfig.criteria, rubricConfig.feedback);
const { criteria } = rubricConfig;
expect(gradeData.criteria).toEqual([
{
orderNum: criteria[0].orderNum,
name: criteria[0].name,
selectedOption: '',
feedback: '',
},
{
orderNum: criteria[1].orderNum,
name: criteria[1].name,
selectedOption: '',
},
{
orderNum: criteria[2].orderNum,
name: criteria[2].name,
selectedOption: '',
feedback: '',
},
]);
describe('gradeData is not passed', () => {
it('adds overall feedback iff is configured for inclusion', () => {
expect(cb(testState, null).overallFeedback).toEqual('');
rubric.feedbackConfig.mockReturnValueOnce(false);
expect(cb(testState, null).overallFeedback).toEqual(undefined);
});
describe('criteria', () => {
it('displays name, orderNum, and feedback per config and empty selection', () => {
expect(cb(testState, null).criteria).toEqual([
{
name: criteria[0].name,
orderNum: criteria[0].orderNum,
feedback: '',
selectedOption: '',
},
{
name: criteria[1].name,
orderNum: criteria[1].orderNum,
selectedOption: '',
},
{
name: criteria[2].name,
orderNum: criteria[2].orderNum,
feedback: '',
selectedOption: '',
},
]);
});
});
});
});
});

View File

@@ -1,15 +1,17 @@
import * as zip from '@zip.js/zip.js';
import FileSaver from 'file-saver';
import { StrictDict } from 'utils';
import { RequestKeys } from 'data/constants/requests';
import { selectors } from 'data/redux';
import { locationId } from 'data/constants/app';
import { stringifyUrl } from 'data/services/lms/utils';
import { networkRequest } from './requests';
import * as module from './download';
export const ERRORS = StrictDict({
fetchFailed: 'Fetch failed',
export const DownloadException = (files) => ({
files,
name: 'DownloadException',
});
/**
@@ -21,22 +23,13 @@ export const genManifest = (files) => files.map(
(file) => `Filename: ${file.name}\nDescription: ${file.description}\nSize: ${file.size}`,
).join('\n\n');
/**
* Returns the zip filename
* @return {string} - zip download file name
*/
export const zipFileName = () => {
const currentDate = new Date().getTime();
return `ora-files-download-${currentDate}.zip`;
};
/**
* Zip the blob output of a set of files with a manifest file.
* @param {obj[]} files - list of file entries with downloadUrl, name, and description
* @param {blob[]} blobs - file content blobs
* @return {Promise} - zip async process promise.
*/
export const zipFiles = async (files, blobs) => {
export const zipFiles = async (files, blobs, username) => {
const zipWriter = new zip.ZipWriter(new zip.BlobWriter('application/zip'));
await zipWriter.add('manifest.txt', new zip.TextReader(module.genManifest(files)));
@@ -50,24 +43,60 @@ export const zipFiles = async (files, blobs) => {
}
const zipFile = await zipWriter.close();
FileSaver.saveAs(zipFile, module.zipFileName());
const zipName = `${username}-${locationId}.zip`;
FileSaver.saveAs(zipFile, zipName);
};
/**
* generate url with additional timestamp for cache busting.
* This is implemented for fixing issue with the browser not
* allowing the user to fetch the same url as the image tag.
* @param {string} url
* @returns {string}
*/
export const getTimeStampUrl = (url) => stringifyUrl(url, {
ora_grading_download_timestamp: new Date().getTime(),
});
/**
* Download a file and return its blob is successful, or null if not.
* @param {obj} file - file entry with downloadUrl
* @return {blob} - file blob or null
* @return {Promise} - file blob or null
*/
export const downloadFile = (file) => fetch(file.downloadUrl).then(resp => (
resp.ok ? resp.blob() : null
));
export const downloadFile = (file) => fetch(
module.getTimeStampUrl(file.downloadUrl),
).then((response) => {
if (!response.ok) {
// This is necessary because some of the error such as 404 does not throw.
// Due to that inconsistency, I have decide to share catch statement like this.
throw new Error(response.statusText);
}
return response.blob();
});
/**
* Download blobs given file objects. Returns a promise map.
* @param {obj[]} files - list of file entries with downloadUrl, name, and description
* @return {Promise[]} - Promise map of download attempts (null for failed fetches)
*/
export const downloadBlobs = (files) => Promise.all(files.map(module.downloadFile));
export const downloadBlobs = async (files) => {
const blobs = [];
const errors = [];
// eslint-disable-next-line no-restricted-syntax
for (const file of files) {
try {
// eslint-disable-next-line no-await-in-loop
blobs.push(await module.downloadFile(file));
} catch (error) {
errors.push(file.name);
}
}
if (errors.length) {
throw DownloadException(errors);
}
return blobs;
};
/**
* Download all files for the selected submission as a zip file.
@@ -75,14 +104,10 @@ export const downloadBlobs = (files) => Promise.all(files.map(module.downloadFil
*/
export const downloadFiles = () => (dispatch, getState) => {
const { files } = selectors.grading.selected.response(getState());
const username = selectors.grading.selected.username(getState());
dispatch(networkRequest({
requestKey: RequestKeys.downloadFiles,
promise: module.downloadBlobs(files).then(blobs => {
if (blobs.some(blob => blob === null)) {
throw Error(ERRORS.fetchFailed);
}
return module.zipFiles(files, blobs);
}),
promise: module.downloadBlobs(files).then(blobs => module.zipFiles(files, blobs, username)),
}));
};

View File

@@ -37,7 +37,10 @@ jest.mock('./requests', () => ({
jest.mock('data/redux', () => ({
selectors: {
grading: {
selected: { response: jest.fn() },
selected: {
response: jest.fn(),
username: jest.fn(),
},
},
},
}));
@@ -55,6 +58,7 @@ describe('download thunkActions', () => {
const files = [mockFile('test-file1.jpg'), mockFile('test-file2.pdf')];
const blobs = ['blob1', 'blob2'];
const response = { files };
const username = 'student-name';
let dispatch;
const getState = () => testState;
describe('genManifest', () => {
@@ -67,13 +71,10 @@ describe('download thunkActions', () => {
);
});
});
describe('zipFileName', () => {
// add tests when name is more nailed down
});
describe('zipFiles', () => {
test('zips files and manifest', () => {
const mockZipWriter = new zip.ZipWriter();
return download.zipFiles(files, blobs).then(() => {
return download.zipFiles(files, blobs, username).then(() => {
expect(mockZipWriter.files).toEqual([
['manifest.txt', mockTextReader],
[files[0].name, mockBlobReader],
@@ -86,63 +87,98 @@ describe('download thunkActions', () => {
});
});
describe('getTimeStampUrl', () => {
it('generate different url every milisecond for cache busting', () => {
const testUrl = 'test/url?param1=true';
const firstGen = download.getTimeStampUrl(testUrl);
// fast forward for 1 milisecond
jest.advanceTimersByTime(1);
const secondGen = download.getTimeStampUrl(testUrl);
expect(firstGen).not.toEqual(secondGen);
});
});
describe('downloadFile', () => {
let fetch;
let getTimeStampUrl;
const blob = 'test-blob';
const file = files[0];
beforeEach(() => {
fetch = window.fetch;
window.fetch = jest.fn();
getTimeStampUrl = download.getTimeStampUrl;
download.getTimeStampUrl = jest.fn();
});
afterEach(() => {
window.fetch = fetch;
download.getTimeStampUrl = getTimeStampUrl;
});
it('returns blob output if successful', () => {
window.fetch.mockReturnValue(Promise.resolve({ ok: true, blob: () => blob }));
return download
.downloadFile(files[0])
.then((val) => expect(val).toEqual(blob));
expect(download.downloadFile(file)).resolves.toEqual(blob);
expect(download.getTimeStampUrl).toBeCalledWith(file.downloadUrl);
});
it('returns null if not successful', () => {
window.fetch.mockReturnValue(Promise.resolve({ ok: false }));
return download
.downloadFile(files[0])
.then((val) => expect(val).toEqual(null));
it('throw if not successful', () => {
const failFetchStatusText = 'failed to fetch';
window.fetch.mockReturnValue(Promise.resolve({ ok: false, statusText: failFetchStatusText }));
expect(() => download.downloadFile(file)).rejects.toThrow(failFetchStatusText);
expect(download.getTimeStampUrl).toBeCalledWith(file.downloadUrl);
});
});
describe('downloadBlobs', () => {
it('returns a joing promise mapping all files to download action', async () => {
download.downloadFile = (file) => Promise.resolve(file.name);
const responses = await download.downloadBlobs(files);
expect(responses).toEqual(files.map((file) => file.name));
let downloadFile;
beforeEach(() => {
downloadFile = download.downloadFile;
download.downloadFile = jest.fn((file) => Promise.resolve(file.name));
});
afterEach(() => { download.downloadFile = downloadFile; });
it('returns a mapping of all files to download action', async () => {
const downloadedBlobs = await download.downloadBlobs(files);
expect(download.downloadFile).toHaveBeenCalledTimes(files.length);
expect(downloadedBlobs.length).toEqual(files.length);
expect(downloadedBlobs).toEqual(files.map(file => file.name));
});
it('returns a mapping of errors from download action', () => {
download.downloadFile = jest.fn(() => { throw new Error(); });
expect(download.downloadBlobs(files)).rejects.toEqual(download.DownloadException(files.map(file => file.name)));
expect(download.downloadFile).toHaveBeenCalledTimes(files.length);
});
});
describe('downloadFiles', () => {
let downloadBlobs;
beforeEach(() => {
dispatch = jest.fn();
selectors.grading.selected.response = () => ({ files });
selectors.grading.selected.username = () => username;
download.zipFiles = jest.fn();
});
it('dispatches network request with downloadFiles key', () => {
downloadBlobs = download.downloadBlobs;
download.downloadBlobs = () => Promise.resolve(blobs);
});
afterEach(() => { download.downloadBlobs = downloadBlobs; });
it('dispatches network request with downloadFiles key', () => {
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
expect(networkRequest.requestKey).toEqual(RequestKeys.downloadFiles);
});
it('dispatches network request for downloadFiles, zipping output of downloadBlobs', () => {
it('dispatches network request for downloadFiles, zipping output of downloadBlobs', async () => {
download.downloadBlobs = () => Promise.resolve(blobs);
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
networkRequest.promise.then(() => {
expect(download.zipFiles).toHaveBeenCalledWith(files, blobs);
});
await networkRequest.promise;
expect(download.zipFiles).toHaveBeenCalledWith(files, blobs, username);
});
it('throws an error on failure', () => {
download.downloadBlobs = () => Promise.all([Promise.resolve(null)]);
it('network request catch all of the errors', () => {
const blobsErrors = ['arbitary', 'error'];
download.downloadBlobs = () => Promise.reject(blobsErrors);
download.downloadFiles()(dispatch, getState);
const { networkRequest } = dispatch.mock.calls[0][0];
expect(networkRequest.promise).rejects.toThrow('Fetch failed');
expect(networkRequest.promise).rejects.toEqual(blobsErrors);
});
});
});

View File

@@ -49,11 +49,8 @@ export const loadSubmission = () => (dispatch, getState) => {
dispatch(actions.grading.loadSubmission({ ...response, submissionUUID }));
if (selectors.grading.selected.isGrading(getState())) {
dispatch(actions.app.setShowRubric(true));
// safety constraints
let { gradeData } = response;
if (gradeData === null || gradeData === undefined || Object.keys(gradeData).length) {
gradeData = selectors.app.emptyGrade(getState());
}
gradeData = selectors.app.fillGradeData(getState(), gradeData);
const lockStatus = selectors.grading.selected.lockStatus(getState());
dispatch(actions.grading.startGrading({ lockStatus, gradeData }));
}
@@ -76,10 +73,7 @@ export const startGrading = () => (dispatch, getState) => {
onSuccess: (response) => {
dispatch(actions.app.setShowRubric(true));
let gradeData = selectors.grading.selected.gradeData(getState());
// safety constraints
if (gradeData === null || gradeData === undefined || Object.keys(gradeData).length) {
gradeData = selectors.app.emptyGrade(getState());
}
gradeData = selectors.app.fillGradeData(getState(), gradeData);
dispatch(actions.grading.startGrading({ ...response, gradeData }));
},
onFailure: (error) => {

View File

@@ -10,7 +10,7 @@ jest.mock('./requests', () => ({
}));
jest.mock('data/redux/app/selectors', () => ({
emptyGrade: (state) => ({ emptyGrade: state }),
fillGradeData: (state, data) => ({ fillGradeData: state, data }),
}));
jest.mock('data/redux/grading/selectors', () => ({
@@ -140,32 +140,22 @@ describe('grading thunkActions', () => {
beforeEach(() => {
dispatch.mockClear();
});
test('dispatches startGrading with selected gradeData if truthy', () => {
const fillString = 'selectors.app.fillGradeData based on selected gradeData';
test(`dispatches startGrading w/ ${fillString}`, () => {
actionArgs.onSuccess(startResponse);
expect(dispatch.mock.calls).toContainEqual([
actions.app.setShowRubric(true),
], [
actions.grading.startGrading({
...startResponse,
gradeData: selectors.grading.selected.gradeData(testState),
gradeData: selectors.app.fillGradeData(
testState,
selectors.grading.selected.gradeData(testState),
),
}),
]);
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
});
test('dispatches startGrading with empty grade if selected gradeData is null', () => {
const emptyGrade = selectors.app.emptyGrade(testState);
const expected = [
actions.grading.startGrading({ ...startResponse, gradeData: emptyGrade }),
];
selectors.grading.selected.gradeData.mockReturnValue(null);
actionArgs.onSuccess({ ...startResponse, gradeData: null });
expect(dispatch.mock.calls).toContainEqual(expected);
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
dispatch.mockClear();
actionArgs.onSuccess({ ...startResponse, gradeData: null });
expect(dispatch.mock.calls).toContainEqual(expected);
expect(dispatch.mock.calls).toContainEqual([actions.app.setShowRubric(true)]);
});
});
});

View File

@@ -97,10 +97,13 @@ const unlockSubmission = (submissionUUID) => client().delete(
* batchUnlockSubmissions(submissionUUIDs)
* @param {string[]} submissionUUIDs - list of submission uuids
*/
const batchUnlockSubmissions = (submissionUUIDs) => {
console.log({ batchUnlockSubmissions: submissionUUIDs });
return new Promise(resolve => resolve());
};
const batchUnlockSubmissions = (submissionUUIDs) => post(
stringifyUrl(
urls.batchUnlockSubmissionsUrl,
{ [paramKeys.oraLocation]: locationId },
),
{ submissionUUIDs },
).then(response => response.data);
/*
* post('api/updateGrade', { submissionUUID, gradeData })

View File

@@ -10,6 +10,7 @@ const oraInitializeUrl = `${baseEsgUrl}initialize`;
const fetchSubmissionUrl = `${baseEsgUrl}submission`;
const fetchSubmissionStatusUrl = `${baseEsgUrl}submission/status`;
const fetchSubmissionLockUrl = `${baseEsgUrl}submission/lock`;
const batchUnlockSubmissionsUrl = `${baseEsgUrl}submission/batch/unlock`;
const updateSubmissionGradeUrl = `${baseEsgUrl}submission/grade`;
const course = (courseId) => `${baseUrl}/courses/${courseId}`;
@@ -25,6 +26,7 @@ export default StrictDict({
fetchSubmissionUrl,
fetchSubmissionStatusUrl,
fetchSubmissionLockUrl,
batchUnlockSubmissionsUrl,
updateSubmissionGradeUrl,
baseUrl,
course,

View File

@@ -10,16 +10,6 @@
"ora-grading.ResponseDisplay.FileRenderer.fileNotFound": "File not found",
"ora-grading.ResponseDisplay.FileRenderer.unknownError": "Unknown errors",
"ora-grading.InfoPopover.alt-text": "Display more info",
"learn.navigation.course.tabs.label": "Course Material",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out",
"header.register.sentenceCase": "Register",
"header.signIn.sentenceCase": "Sign in",
"ora-grading.CriterionFeedback.addCommentsLabel": "Add comments",
"ora-grading.CriterionFeedback.commentsLabel": "Comments",
"ora-grading.CriterionFeedback.optional": "(Optional)",
@@ -53,6 +43,8 @@
"ora-grading.ResponseDisplay.SubmissionFiles.downloading": "Downloading",
"ora-grading.ResponseDisplay.SubmissionFiles.downloaded": "Downloaded!",
"ora-grading.ResponseDisplay.SubmissionFiles.retryDownload": "Retry download",
"ora-grading.ResponseDisplay.SubmissionFiles.submissionFile": "Submission Files",
"ora-grading.ResponseDisplay.SubmissionFiles.fileSizeExceed": "Exceeded the allow download size",
"ora-grading.ReviewActions.overrideConfirmTitle": "Are you sure you want to override this grade?",
"ora-grading.ReviewActions.overrideConfirmWarning": "This cannot be undone. The learner may have already received their grade.",
"ora-grading.ReviewActions.overrideConfirmContinue": "Continue grade override",
@@ -93,6 +85,7 @@
"ora-grading.ReviewModal.errorDownloadFailed": "Couldn't download files",
"ora-grading.ReviewModal.errorDownloadFailedContent": "We're sorry, something went wrong when we tried to download these files. Please try again.",
"ora-grading.ReviewModal.errorRetryDownload": "Retry download",
"ora-grading.ReviewModal.errorDownloadFailedFiles": "Failed files:",
"ora-grading.Rubric.gradeSubmitted": "Grade Submitted",
"ora-grading.Rubric.rubric": "Rubric",
"ora-grading.Rubric.submitGrade": "Submit grade",

View File

@@ -10,16 +10,6 @@
"ora-grading.ResponseDisplay.FileRenderer.fileNotFound": "Archivo no encontrado",
"ora-grading.ResponseDisplay.FileRenderer.unknownError": "Errores desconocidos",
"ora-grading.InfoPopover.alt-text": "Mostrar más información",
"learn.navigation.course.tabs.label": "Material del Curso",
"header.menu.dashboard.label": "Panel de Control",
"header.help.label": "Ayuda",
"header.menu.profile.label": "Perfil",
"header.menu.account.label": "Cuenta",
"header.menu.orderHistory.label": "Historial de órdenes",
"header.navigation.skipNavLink": "Dirígete al contenido principal.",
"header.menu.signOut.label": "Cerrar sesión",
"header.register.sentenceCase": "Registrarse",
"header.signIn.sentenceCase": "Iniciar sesión",
"ora-grading.CriterionFeedback.addCommentsLabel": "Añadir comentarios",
"ora-grading.CriterionFeedback.commentsLabel": "Comentarios",
"ora-grading.CriterionFeedback.optional": "(Opcional)",
@@ -53,6 +43,8 @@
"ora-grading.ResponseDisplay.SubmissionFiles.downloading": "Descargando",
"ora-grading.ResponseDisplay.SubmissionFiles.downloaded": "¡Descargado!",
"ora-grading.ResponseDisplay.SubmissionFiles.retryDownload": "Vuelva a intentar descargar",
"ora-grading.ResponseDisplay.SubmissionFiles.submissionFile": "Submission Files",
"ora-grading.ResponseDisplay.SubmissionFiles.fileSizeExceed": "Exceeded the allow download size",
"ora-grading.ReviewActions.overrideConfirmTitle": "¿Está seguro de que desea anular esta calificación?",
"ora-grading.ReviewActions.overrideConfirmWarning": "Esto no se puede deshacer. Es posible que el alumno ya haya recibido su calificación.",
"ora-grading.ReviewActions.overrideConfirmContinue": "Continuar anulación de calificación",
@@ -93,6 +85,7 @@
"ora-grading.ReviewModal.errorDownloadFailed": "No se pudieron descargar los archivos",
"ora-grading.ReviewModal.errorDownloadFailedContent": "Lo sentimos, algo salió mal cuando intentamos descargar estos archivos. Inténtalo de nuevo.",
"ora-grading.ReviewModal.errorRetryDownload": "Vuelva a intentar descargar",
"ora-grading.ReviewModal.errorDownloadFailedFiles": "Failed files:",
"ora-grading.Rubric.gradeSubmitted": "Calificación enviada",
"ora-grading.Rubric.rubric": "Rúbrica",
"ora-grading.Rubric.submitGrade": "Enviar calificación",

View File

@@ -1,7 +1,7 @@
{
"ora-grading.demoAlert.warningMessage": "Grade submission is disabled in the Demo mode of the new ORA Staff Grader.",
"ora-grading.demoAlert.confirm": "Confirm",
"ora-grading.demoAlert.title": "Demo submit prevented",
"ora-grading.demoAlert.warningMessage": "La soumission des notes est désactivée dans le mode démonstration du nouveau correcteur ORA.",
"ora-grading.demoAlert.confirm": "Confirmer",
"ora-grading.demoAlert.title": "Soumission de démonstration empêchée",
"ora-grading.FilePopoverContent.filePopoverNameTitle": "Nom du fichier",
"ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle": "Description du fichier",
"ora-grading.FilePopoverCellContent.fileSizeTitle": "Taille du fichier",
@@ -10,24 +10,14 @@
"ora-grading.ResponseDisplay.FileRenderer.fileNotFound": "Fichier introuvable",
"ora-grading.ResponseDisplay.FileRenderer.unknownError": "Erreurs inconnues",
"ora-grading.InfoPopover.alt-text": "Afficher plus d&#39;informations",
"learn.navigation.course.tabs.label": "Matériel de cours",
"header.menu.dashboard.label": "Tableau de bord",
"header.help.label": "Aide",
"header.menu.profile.label": "Profil",
"header.menu.account.label": "Compte",
"header.menu.orderHistory.label": "Historique des commandes",
"header.navigation.skipNavLink": "Passer au contenu principal",
"header.menu.signOut.label": "Se déconnecter",
"header.register.sentenceCase": "S'inscrire",
"header.signIn.sentenceCase": "Se connecter.",
"ora-grading.CriterionFeedback.addCommentsLabel": "Ajoutez des commentaires",
"ora-grading.CriterionFeedback.commentsLabel": "Commentaires",
"ora-grading.CriterionFeedback.optional": "(Optionnel)",
"ora-grading.RadioCriterion.optionPoints": "{points} points",
"ora-grading.RadioCriterion.rubricSelectedError": "La sélection de la rubrique est requise",
"ora-grading.CriterionFeedback.criterionFeedbackError": "Le feedback est requis",
"ora-grading.ReviewModal.demoHeading": "Demo Mode",
"ora-grading.ReviewModal.demoMessage": "You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature.",
"ora-grading.ReviewModal.demoHeading": "Mode de démonstration",
"ora-grading.ReviewModal.demoMessage": "Vous utilisez le mode de démonstration de la nouvelle interface du nouveau correcteur ORA amélioré. Vous ne pourrez pas soumettre de notes tant que vous n'aurez pas activé la fonctionnalité.",
"ora-grading.ListView.ListViewBreadcrumbs.backToResponses": "Retour à toutes les réponses ouvertes",
"ora-grading.ListView.noResultsFoundTitle": "Rien ici encore",
"ora-grading.ListView.noResultsFoundBody": "Lorsque les apprenants soumettront des réponses, elles apparaîtront ici",
@@ -53,6 +43,8 @@
"ora-grading.ResponseDisplay.SubmissionFiles.downloading": "Téléchargement",
"ora-grading.ResponseDisplay.SubmissionFiles.downloaded": "Téléchargé !",
"ora-grading.ResponseDisplay.SubmissionFiles.retryDownload": "Réessayez le téléchargement",
"ora-grading.ResponseDisplay.SubmissionFiles.submissionFile": "Submission Files",
"ora-grading.ResponseDisplay.SubmissionFiles.fileSizeExceed": "Exceeded the allow download size",
"ora-grading.ReviewActions.overrideConfirmTitle": "Êtes-vous sûr de vouloir remplacer cette note ?",
"ora-grading.ReviewActions.overrideConfirmWarning": "Ça ne peut pas être annulé. L&#39;apprenant peut avoir déjà reçu sa note.",
"ora-grading.ReviewActions.overrideConfirmContinue": "Continuer le remplacement de la note",
@@ -76,7 +68,7 @@
"ora-grading.ReviewModal.goBack": "Retour",
"ora-grading.ReviewModal.CloseReviewConfirmModal.confirmText": "Fermer le modal",
"ora-grading.ReviewModal.loadingResponse": "Chargement de la réponse",
"ora-grading.ReviewModal.demoTitleMessage": "Grading Demo",
"ora-grading.ReviewModal.demoTitleMessage": "Démonstration de correcteur",
"ora-grading.ReviewModal.loadErrorHeading": "Erreur lors du chargement des soumissions",
"ora-grading.ReviewModal.loadErrorMessage1": "Une erreur s&#39;est produite lors du chargement de cette soumission. Essayez de recharger cette soumission.",
"ora-grading.ReviewModal.reloadSubmission": "Recharger la soumission",
@@ -93,6 +85,7 @@
"ora-grading.ReviewModal.errorDownloadFailed": "Impossible de télécharger les fichiers",
"ora-grading.ReviewModal.errorDownloadFailedContent": "Nous sommes désolés, une erreur s&#39;est produite lorsque nous avons essayé de télécharger ces fichiers. Veuillez réessayer.",
"ora-grading.ReviewModal.errorRetryDownload": "Réessayez le téléchargement",
"ora-grading.ReviewModal.errorDownloadFailedFiles": "Fichiers ayant échoué :",
"ora-grading.Rubric.gradeSubmitted": "Note soumise",
"ora-grading.Rubric.rubric": "Rubrique",
"ora-grading.Rubric.submitGrade": "Soumettre la note",

View File

@@ -10,16 +10,6 @@
"ora-grading.ResponseDisplay.FileRenderer.fileNotFound": "File not found",
"ora-grading.ResponseDisplay.FileRenderer.unknownError": "Unknown errors",
"ora-grading.InfoPopover.alt-text": "Display more info",
"learn.navigation.course.tabs.label": "Course Material",
"header.menu.dashboard.label": "Dashboard",
"header.help.label": "Help",
"header.menu.profile.label": "Profile",
"header.menu.account.label": "Account",
"header.menu.orderHistory.label": "Order History",
"header.navigation.skipNavLink": "Skip to main content.",
"header.menu.signOut.label": "Sign Out",
"header.register.sentenceCase": "Register",
"header.signIn.sentenceCase": "Sign in",
"ora-grading.CriterionFeedback.addCommentsLabel": "Add comments",
"ora-grading.CriterionFeedback.commentsLabel": "Comments",
"ora-grading.CriterionFeedback.optional": "(Optional)",
@@ -53,6 +43,8 @@
"ora-grading.ResponseDisplay.SubmissionFiles.downloading": "Downloading",
"ora-grading.ResponseDisplay.SubmissionFiles.downloaded": "Downloaded!",
"ora-grading.ResponseDisplay.SubmissionFiles.retryDownload": "Retry download",
"ora-grading.ResponseDisplay.SubmissionFiles.submissionFile": "Submission Files",
"ora-grading.ResponseDisplay.SubmissionFiles.fileSizeExceed": "Exceeded the allow download size",
"ora-grading.ReviewActions.overrideConfirmTitle": "Are you sure you want to override this grade?",
"ora-grading.ReviewActions.overrideConfirmWarning": "This cannot be undone. The learner may have already received their grade.",
"ora-grading.ReviewActions.overrideConfirmContinue": "Continue grade override",
@@ -93,6 +85,7 @@
"ora-grading.ReviewModal.errorDownloadFailed": "Couldn't download files",
"ora-grading.ReviewModal.errorDownloadFailedContent": "We're sorry, something went wrong when we tried to download these files. Please try again.",
"ora-grading.ReviewModal.errorRetryDownload": "Retry download",
"ora-grading.ReviewModal.errorDownloadFailedFiles": "Failed files:",
"ora-grading.Rubric.gradeSubmitted": "Grade Submitted",
"ora-grading.Rubric.rubric": "Rubric",
"ora-grading.Rubric.submitGrade": "Submit grade",

View File

@@ -11,8 +11,12 @@ import {
APP_INIT_ERROR,
initialize,
subscribe,
mergeConfig,
} from '@edx/frontend-platform';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as headerMesssages } from '@edx/frontend-component-header';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import messages from './i18n';
@@ -38,8 +42,16 @@ subscribe(APP_INIT_ERROR, (error) => {
});
initialize({
handlers: {
config: () => {
mergeConfig({
SUPPORT_URL: process.env.SUPPORT_URL || null,
}, 'OraGradingAppConfig');
},
},
messages: [
messages,
headerMesssages,
footerMessages,
],
requireAuthenticatedUser: true,

View File

@@ -6,7 +6,9 @@ import {
initialize,
subscribe,
} from '@edx/frontend-platform';
import { messages as footerMessages } from '@edx/frontend-component-footer';
import { messages as headerMesssages } from '@edx/frontend-component-header';
import appMessages from './i18n';
import App from './App';
@@ -36,17 +38,18 @@ describe('app registry', () => {
afterAll(() => {
window.document.getElementById = getElement;
});
test('subscribe is called for APP_READY, linking App to root element', () => {
const callArgs = subscribe.mock.calls[0];
const callArgs = subscribe.mock.calls[1];
expect(callArgs[0]).toEqual(APP_READY);
expect(callArgs[1]()).toEqual(
ReactDOM.render(<App />, document.getElementById('root')),
);
});
test('initialize is called with footerMessages and requireAuthenticatedUser', () => {
expect(initialize).toHaveBeenCalledWith({
messages: [appMessages, footerMessages],
requireAuthenticatedUser: true,
});
expect(initialize).toHaveBeenCalledTimes(1);
const initializeArg = initialize.mock.calls[0][0];
expect(initializeArg.messages).toEqual([appMessages, headerMesssages, footerMessages]);
expect(initializeArg.requireAuthenticatedUser).toEqual(true);
});
});

View File

@@ -1,7 +1,6 @@
import InfoPopover from 'components/InfoPopover/messages';
import ResponseDisplay from 'containers/ResponseDisplay/messages';
import ResponseDisplayComponents from 'containers/ResponseDisplay/components/messages';
import CourseHeader from 'containers/CourseHeader/messages';
import CriterionContainer from 'containers/CriterionContainer/messages';
import ListView from 'containers/ListView/messages';
import ReviewActions from 'containers/ReviewActions/messages';
@@ -20,7 +19,6 @@ export default {
InfoPopover: mapMessages(InfoPopover),
ResponseDisplay: mapMessages(ResponseDisplay),
ResponseDisplayComponents: mapMessages(ResponseDisplayComponents),
CourseHeader: mapMessages(CourseHeader),
CriterionContainer: mapMessages(CriterionContainer),
ListView: mapMessages(ListView),
ReviewActions: mapMessages(ReviewActions),