Compare commits
9 Commits
release/ul
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2c6aae3b | ||
|
|
d4c6d93c26 | ||
|
|
0ff61e11d5 | ||
|
|
d9e5ae0c80 | ||
|
|
3c03358d4e | ||
|
|
f7e6e30d99 | ||
|
|
ae365b6951 | ||
|
|
729cb40c66 | ||
|
|
bc4abcdeef |
16
README.rst
16
README.rst
@@ -26,18 +26,14 @@ Getting Started
|
|||||||
Prerequisites
|
Prerequisites
|
||||||
=============
|
=============
|
||||||
|
|
||||||
The `devstack`_ is currently recommended as a development environment for your
|
`Tutor`_ is currently recommended as a development environment for your
|
||||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
new MFE. Please refer
|
||||||
everything you need as a companion to this frontend.
|
|
||||||
|
|
||||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
|
||||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||||
|
|
||||||
.. _Devstack: https://github.com/openedx/devstack
|
|
||||||
|
|
||||||
.. _Tutor: https://github.com/overhangio/tutor
|
.. _Tutor: https://github.com/overhangio/tutor
|
||||||
|
|
||||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||||
|
|
||||||
|
|
||||||
Plugins
|
Plugins
|
||||||
=======
|
=======
|
||||||
@@ -60,9 +56,9 @@ First, clone the repo, install code prerequisites, and start the app.
|
|||||||
|
|
||||||
``git clone git@github.com:openedx/frontend-app-ora-grading.git``
|
``git clone git@github.com:openedx/frontend-app-ora-grading.git``
|
||||||
|
|
||||||
2. Use node v18.x.
|
2. Use the version of Node specified in the ``.nvmrc`` file.
|
||||||
|
|
||||||
The current version of the micro-frontend build scripts support node 18.
|
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
|
||||||
Using other major versions of node *may* work, but this is unsupported. For
|
Using other major versions of node *may* work, but this is unsupported. For
|
||||||
convenience, this repository includes an .nvmrc file to help in setting the
|
convenience, this repository includes an .nvmrc file to help in setting the
|
||||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||||
|
|||||||
5301
package-lock.json
generated
5301
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,7 +29,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||||
"@edx/frontend-component-footer": "^14.6.0",
|
"@edx/frontend-component-footer": "^14.6.0",
|
||||||
"@edx/frontend-component-header": "^6.2.0",
|
"@edx/frontend-component-header": "^8.0.0",
|
||||||
"@edx/frontend-platform": "^8.3.1",
|
"@edx/frontend-platform": "^8.3.1",
|
||||||
"@edx/openedx-atlas": "^0.6.0",
|
"@edx/openedx-atlas": "^0.6.0",
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export const fetchFile = async ({
|
|||||||
onSuccess();
|
onSuccess();
|
||||||
setContent(data);
|
setContent(data);
|
||||||
})
|
})
|
||||||
.catch((e) => onError(e.response.status));
|
.catch((e) => onError(e.response?.status));
|
||||||
|
|
||||||
export const rendererHooks = ({ url, onError, onSuccess }) => {
|
export const rendererHooks = ({ url, onError, onSuccess }) => {
|
||||||
const [content, setContent] = module.state.content('');
|
const [content, setContent] = module.state.content('');
|
||||||
|
|||||||
30
src/containers/ResponseDisplay/PromptDisplay.jsx
Normal file
30
src/containers/ResponseDisplay/PromptDisplay.jsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Collapsible } from '@openedx/paragon';
|
||||||
|
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||||
|
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import messages from './messages';
|
||||||
|
|
||||||
|
const PromptDisplay = ({
|
||||||
|
prompt,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const msg = intl.formatMessage(messages.promptCollapsibleHeader);
|
||||||
|
return (
|
||||||
|
<div className="prompt-display">
|
||||||
|
<Collapsible
|
||||||
|
defaultOpen
|
||||||
|
styling="card-lg"
|
||||||
|
title={<h3>{msg}</h3>}
|
||||||
|
>
|
||||||
|
{ prompt }
|
||||||
|
</Collapsible>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
PromptDisplay.propTypes = {
|
||||||
|
prompt: PropTypes.string.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PromptDisplay;
|
||||||
@@ -4,6 +4,14 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
|
|
||||||
|
.prompt-display-single {
|
||||||
|
padding: var(--pgn-spacing-spacer-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prompt-display-multiple > .collapsible-basic .collapsible-trigger {
|
||||||
|
text-decoration: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.submission-files {
|
.submission-files {
|
||||||
.submission-files-title {
|
.submission-files-title {
|
||||||
padding: var(--pgn-spacing-spacer-3);
|
padding: var(--pgn-spacing-spacer-3);
|
||||||
@@ -42,6 +50,10 @@
|
|||||||
padding: var(--pgn-spacing-spacer-3) 0;
|
padding: var(--pgn-spacing-spacer-3) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.response-display-card {
|
||||||
|
margin: var(--pgn-spacing-spacer-3) 0;
|
||||||
|
}
|
||||||
|
|
||||||
.response-display-text-content {
|
.response-display-text-content {
|
||||||
white-space: pre-line;
|
white-space: pre-line;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { fileUploadResponseOptions } from 'data/services/lms/constants';
|
|||||||
import { getConfig } from '@edx/frontend-platform';
|
import { getConfig } from '@edx/frontend-platform';
|
||||||
import SubmissionFiles from './SubmissionFiles';
|
import SubmissionFiles from './SubmissionFiles';
|
||||||
import PreviewDisplay from './PreviewDisplay';
|
import PreviewDisplay from './PreviewDisplay';
|
||||||
|
import PromptDisplay from './PromptDisplay';
|
||||||
import './ResponseDisplay.scss';
|
import './ResponseDisplay.scss';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,13 +26,13 @@ export class ResponseDisplay extends React.Component {
|
|||||||
this.purify = createDOMPurify(window);
|
this.purify = createDOMPurify(window);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get prompts() {
|
||||||
|
return this.props.prompts.map((item) => this.formattedHtml(item));
|
||||||
|
}
|
||||||
|
|
||||||
get textContents() {
|
get textContents() {
|
||||||
const { text } = this.props.response;
|
const { text } = this.props.response;
|
||||||
|
const formattedText = text.map((item) => this.formattedHtml(item));
|
||||||
const formattedText = text
|
|
||||||
.map((item) => item.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`))
|
|
||||||
.map((item) => parse(this.purify.sanitize(item)));
|
|
||||||
|
|
||||||
return formattedText;
|
return formattedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,17 +46,28 @@ export class ResponseDisplay extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
formattedHtml(text) {
|
||||||
|
const cleanedText = text.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`);
|
||||||
|
return parse(this.purify.sanitize(cleanedText));
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { prompts } = this;
|
||||||
|
const multiPrompt = prompts.length > 1;
|
||||||
return (
|
return (
|
||||||
<div className="response-display">
|
<div className="response-display">
|
||||||
|
{!multiPrompt && <PromptDisplay prompt={prompts[0]} />}
|
||||||
{this.allowFileUpload && <SubmissionFiles files={this.submittedFiles} data-testid="submission-files" />}
|
{this.allowFileUpload && <SubmissionFiles files={this.submittedFiles} data-testid="submission-files" />}
|
||||||
{this.allowFileUpload && <PreviewDisplay files={this.submittedFiles} data-testid="allow-file-upload" />}
|
{this.allowFileUpload && <PreviewDisplay files={this.submittedFiles} data-testid="allow-file-upload" />}
|
||||||
{
|
{
|
||||||
/* eslint-disable react/no-array-index-key */
|
/* eslint-disable react/no-array-index-key */
|
||||||
this.textContents.map((textContent, index) => (
|
this.textContents.map((textContent, index) => (
|
||||||
<Card key={index}>
|
<>
|
||||||
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
|
{ multiPrompt && <PromptDisplay prompt={prompts[index]} /> }
|
||||||
</Card>
|
<Card className="response-display-card" key={index}>
|
||||||
|
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
|
||||||
|
</Card>
|
||||||
|
</>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -71,6 +82,7 @@ ResponseDisplay.defaultProps = {
|
|||||||
},
|
},
|
||||||
fileUploadResponseConfig: fileUploadResponseOptions.none,
|
fileUploadResponseConfig: fileUploadResponseOptions.none,
|
||||||
};
|
};
|
||||||
|
|
||||||
ResponseDisplay.propTypes = {
|
ResponseDisplay.propTypes = {
|
||||||
response: PropTypes.shape({
|
response: PropTypes.shape({
|
||||||
text: PropTypes.arrayOf(PropTypes.string),
|
text: PropTypes.arrayOf(PropTypes.string),
|
||||||
@@ -83,11 +95,13 @@ ResponseDisplay.propTypes = {
|
|||||||
fileUploadResponseConfig: PropTypes.oneOf(
|
fileUploadResponseConfig: PropTypes.oneOf(
|
||||||
Object.values(fileUploadResponseOptions),
|
Object.values(fileUploadResponseOptions),
|
||||||
),
|
),
|
||||||
|
prompts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const mapStateToProps = (state) => ({
|
export const mapStateToProps = (state) => ({
|
||||||
response: selectors.grading.selected.response(state),
|
response: selectors.grading.selected.response(state),
|
||||||
fileUploadResponseConfig: selectors.app.ora.fileUploadResponseConfig(state),
|
fileUploadResponseConfig: selectors.app.ora.fileUploadResponseConfig(state),
|
||||||
|
prompts: selectors.app.ora.prompts(state),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const mapDispatchToProps = {};
|
export const mapDispatchToProps = {};
|
||||||
|
|||||||
@@ -13,11 +13,16 @@ jest.mock('data/redux', () => ({
|
|||||||
app: {
|
app: {
|
||||||
ora: {
|
ora: {
|
||||||
fileUploadResponseConfig: jest.fn((state) => state.fileUploadResponseConfig || 'optional'),
|
fileUploadResponseConfig: jest.fn((state) => state.fileUploadResponseConfig || 'optional'),
|
||||||
|
prompts: jest.fn((state) => state.prompts || ['prompt']),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('./PromptDisplay', () => jest.fn(({ prompt }) => (
|
||||||
|
<div data-testid="prompt-display">Prompt: {prompt}</div>
|
||||||
|
)));
|
||||||
|
|
||||||
jest.mock('./SubmissionFiles', () => jest.fn(({ files }) => (
|
jest.mock('./SubmissionFiles', () => jest.fn(({ files }) => (
|
||||||
<div data-testid="submission-files">Files: {files.length}</div>
|
<div data-testid="submission-files">Files: {files.length}</div>
|
||||||
)));
|
)));
|
||||||
@@ -50,6 +55,7 @@ describe('ResponseDisplay', () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
fileUploadResponseConfig: 'optional',
|
fileUploadResponseConfig: 'optional',
|
||||||
|
prompts: ['prompt one', 'prompt two'],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -100,6 +106,16 @@ describe('ResponseDisplay', () => {
|
|||||||
const textContents = container.querySelectorAll('.response-display-text-content');
|
const textContents = container.querySelectorAll('.response-display-text-content');
|
||||||
expect(textContents).toHaveLength(0);
|
expect(textContents).toHaveLength(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('displays single prompt when only one prompt', () => {
|
||||||
|
render(<ResponseDisplay {...defaultProps} prompts={['only one prompt']} />);
|
||||||
|
expect(screen.queryAllByTestId('prompt-display')).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays multiple prompts when there are multiple prompts', () => {
|
||||||
|
render(<ResponseDisplay {...defaultProps} />);
|
||||||
|
expect(screen.queryAllByTestId('prompt-display')).toHaveLength(2);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mapStateToProps', () => {
|
describe('mapStateToProps', () => {
|
||||||
@@ -109,6 +125,7 @@ describe('ResponseDisplay', () => {
|
|||||||
files: ['file1', 'file2'],
|
files: ['file1', 'file2'],
|
||||||
},
|
},
|
||||||
fileUploadResponseConfig: 'required',
|
fileUploadResponseConfig: 'required',
|
||||||
|
prompts: ['prompt'],
|
||||||
};
|
};
|
||||||
|
|
||||||
it('maps response from grading.selected.response selector', () => {
|
it('maps response from grading.selected.response selector', () => {
|
||||||
@@ -120,5 +137,10 @@ describe('ResponseDisplay', () => {
|
|||||||
const mapped = mapStateToProps(testState);
|
const mapped = mapStateToProps(testState);
|
||||||
expect(mapped.fileUploadResponseConfig).toEqual(selectors.app.ora.fileUploadResponseConfig(testState));
|
expect(mapped.fileUploadResponseConfig).toEqual(selectors.app.ora.fileUploadResponseConfig(testState));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('maps prompts from app.ora.prompts selector', () => {
|
||||||
|
const mapped = mapStateToProps(testState);
|
||||||
|
expect(mapped.prompts).toEqual(selectors.app.ora.prompts(testState));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ const messages = defineMessages({
|
|||||||
defaultMessage: 'Exceeded the allow download size',
|
defaultMessage: 'Exceeded the allow download size',
|
||||||
description: 'Exceed the allow download size error message',
|
description: 'Exceed the allow download size error message',
|
||||||
},
|
},
|
||||||
|
promptCollapsibleHeader: {
|
||||||
|
id: 'ora-grading.ResponseDisplay.Prompt.collapsibleHeader',
|
||||||
|
defaultMessage: 'Prompt',
|
||||||
|
description: 'Header for a collapsible that displays the assignment prompt',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default messages;
|
export default messages;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const initialState = {
|
|||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
isGrading: false,
|
isGrading: false,
|
||||||
oraMetadata: {
|
oraMetadata: {
|
||||||
prompt: '',
|
prompts: [],
|
||||||
name: '',
|
name: '',
|
||||||
type: '',
|
type: '',
|
||||||
rubricConfig: null,
|
rubricConfig: null,
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ describe('app reducer', () => {
|
|||||||
});
|
});
|
||||||
test('populated, but empty ora metadata', () => {
|
test('populated, but empty ora metadata', () => {
|
||||||
const data = initialState.oraMetadata;
|
const data = initialState.oraMetadata;
|
||||||
expect(data.prompt).toEqual('');
|
expect(data.prompts).toEqual([]);
|
||||||
expect(data.name).toEqual('');
|
expect(data.name).toEqual('');
|
||||||
expect(data.type).toEqual('');
|
expect(data.type).toEqual('');
|
||||||
expect(data.rubricConfig).toEqual(null);
|
expect(data.rubricConfig).toEqual(null);
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ export const ora = {
|
|||||||
*/
|
*/
|
||||||
name: oraMetadataSelector(data => data.name),
|
name: oraMetadataSelector(data => data.name),
|
||||||
/**
|
/**
|
||||||
* Returns the ORA Prompt
|
* Returns the ORA Prompts
|
||||||
* @return {string} - ORA prompt
|
* @return {array[]} - List of ORA prompts
|
||||||
*/
|
*/
|
||||||
prompt: oraMetadataSelector(data => data.prompt),
|
prompts: oraMetadataSelector(data => (data.prompts ? data.prompts.map((oraPrompt) => oraPrompt.description) : [])),
|
||||||
/**
|
/**
|
||||||
* Returns the ORA type
|
* Returns the ORA type
|
||||||
* @return {string} - ORA type (team vs individual)
|
* @return {string} - ORA type (team vs individual)
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ const testState = {
|
|||||||
},
|
},
|
||||||
oraMetadata: {
|
oraMetadata: {
|
||||||
name: 'test-ora-name',
|
name: 'test-ora-name',
|
||||||
prompt: 'test-ora-prompt',
|
prompts: [
|
||||||
|
{ description: 'test-ora-prompt' },
|
||||||
|
{ description: 'test-second-prompt' },
|
||||||
|
],
|
||||||
type: 'test-ora-type',
|
type: 'test-ora-type',
|
||||||
fileUploadResponseConfig: 'file-upload-response-config',
|
fileUploadResponseConfig: 'file-upload-response-config',
|
||||||
rubricConfig: {
|
rubricConfig: {
|
||||||
@@ -102,8 +105,8 @@ describe('app selectors unit tests', () => {
|
|||||||
test('ora.name selector returns name from oraMetadata', () => {
|
test('ora.name selector returns name from oraMetadata', () => {
|
||||||
testOraSelector(selectors.ora.name, oraMetadata.name);
|
testOraSelector(selectors.ora.name, oraMetadata.name);
|
||||||
});
|
});
|
||||||
test('ora.prompt selector returns prompt from oraMetadata', () => {
|
test('ora.prompts selector returns prompts from oraMetadata', () => {
|
||||||
testOraSelector(selectors.ora.prompt, oraMetadata.prompt);
|
testOraSelector(selectors.ora.prompts, ['test-ora-prompt', 'test-second-prompt']);
|
||||||
});
|
});
|
||||||
test('ora.type selector returns type from oraMetadata', () => {
|
test('ora.type selector returns type from oraMetadata', () => {
|
||||||
testOraSelector(selectors.ora.type, oraMetadata.type);
|
testOraSelector(selectors.ora.type, oraMetadata.type);
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
/**
|
/**
|
||||||
* get('/api/initialize', { oraLocation })
|
* get('/api/initialize', { oraLocation })
|
||||||
* @return {
|
* @return {
|
||||||
* oraMetadata: { name, prompt, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig },
|
* oraMetadata: { name, prompts, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig },
|
||||||
* courseMetadata: { courseOrg, courseName, courseNumber, courseId },
|
* courseMetadata: { courseOrg, courseName, courseNumber, courseId },
|
||||||
* submissions: {
|
* submissions: {
|
||||||
* [submissionUUID]: {
|
* [submissionUUID]: {
|
||||||
|
|||||||
Reference in New Issue
Block a user