Merge pull request #471 from openedx/jkantor/prompt
feat: add prompt to grading screen
This commit is contained in:
@@ -18,7 +18,7 @@ export const fetchFile = async ({
|
||||
onSuccess();
|
||||
setContent(data);
|
||||
})
|
||||
.catch((e) => onError(e.response.status));
|
||||
.catch((e) => onError(e.response?.status));
|
||||
|
||||
export const rendererHooks = ({ url, onError, onSuccess }) => {
|
||||
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;
|
||||
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-title {
|
||||
padding: var(--pgn-spacing-spacer-3);
|
||||
@@ -42,6 +50,10 @@
|
||||
padding: var(--pgn-spacing-spacer-3) 0;
|
||||
}
|
||||
|
||||
.response-display-card {
|
||||
margin: var(--pgn-spacing-spacer-3) 0;
|
||||
}
|
||||
|
||||
.response-display-text-content {
|
||||
white-space: pre-line;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -14,7 +14,7 @@ import { fileUploadResponseOptions } from 'data/services/lms/constants';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import SubmissionFiles from './SubmissionFiles';
|
||||
import PreviewDisplay from './PreviewDisplay';
|
||||
|
||||
import PromptDisplay from './PromptDisplay';
|
||||
import './ResponseDisplay.scss';
|
||||
|
||||
/**
|
||||
@@ -26,13 +26,13 @@ export class ResponseDisplay extends React.Component {
|
||||
this.purify = createDOMPurify(window);
|
||||
}
|
||||
|
||||
get prompts() {
|
||||
return this.props.prompts.map((item) => this.formattedHtml(item));
|
||||
}
|
||||
|
||||
get textContents() {
|
||||
const { text } = this.props.response;
|
||||
|
||||
const formattedText = text
|
||||
.map((item) => item.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`))
|
||||
.map((item) => parse(this.purify.sanitize(item)));
|
||||
|
||||
const formattedText = text.map((item) => this.formattedHtml(item));
|
||||
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() {
|
||||
const { prompts } = this;
|
||||
const multiPrompt = prompts.length > 1;
|
||||
return (
|
||||
<div className="response-display">
|
||||
{!multiPrompt && <PromptDisplay prompt={prompts[0]} />}
|
||||
{this.allowFileUpload && <SubmissionFiles files={this.submittedFiles} data-testid="submission-files" />}
|
||||
{this.allowFileUpload && <PreviewDisplay files={this.submittedFiles} data-testid="allow-file-upload" />}
|
||||
{
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
this.textContents.map((textContent, index) => (
|
||||
<Card key={index}>
|
||||
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
|
||||
</Card>
|
||||
<>
|
||||
{ multiPrompt && <PromptDisplay prompt={prompts[index]} /> }
|
||||
<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>
|
||||
@@ -71,6 +82,7 @@ ResponseDisplay.defaultProps = {
|
||||
},
|
||||
fileUploadResponseConfig: fileUploadResponseOptions.none,
|
||||
};
|
||||
|
||||
ResponseDisplay.propTypes = {
|
||||
response: PropTypes.shape({
|
||||
text: PropTypes.arrayOf(PropTypes.string),
|
||||
@@ -83,11 +95,13 @@ ResponseDisplay.propTypes = {
|
||||
fileUploadResponseConfig: PropTypes.oneOf(
|
||||
Object.values(fileUploadResponseOptions),
|
||||
),
|
||||
prompts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
response: selectors.grading.selected.response(state),
|
||||
fileUploadResponseConfig: selectors.app.ora.fileUploadResponseConfig(state),
|
||||
prompts: selectors.app.ora.prompts(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
@@ -13,11 +13,16 @@ jest.mock('data/redux', () => ({
|
||||
app: {
|
||||
ora: {
|
||||
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 }) => (
|
||||
<div data-testid="submission-files">Files: {files.length}</div>
|
||||
)));
|
||||
@@ -50,6 +55,7 @@ describe('ResponseDisplay', () => {
|
||||
],
|
||||
},
|
||||
fileUploadResponseConfig: 'optional',
|
||||
prompts: ['prompt one', 'prompt two'],
|
||||
};
|
||||
|
||||
beforeAll(() => {
|
||||
@@ -100,6 +106,16 @@ describe('ResponseDisplay', () => {
|
||||
const textContents = container.querySelectorAll('.response-display-text-content');
|
||||
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', () => {
|
||||
@@ -109,6 +125,7 @@ describe('ResponseDisplay', () => {
|
||||
files: ['file1', 'file2'],
|
||||
},
|
||||
fileUploadResponseConfig: 'required',
|
||||
prompts: ['prompt'],
|
||||
};
|
||||
|
||||
it('maps response from grading.selected.response selector', () => {
|
||||
@@ -120,5 +137,10 @@ describe('ResponseDisplay', () => {
|
||||
const mapped = mapStateToProps(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',
|
||||
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;
|
||||
|
||||
@@ -11,7 +11,7 @@ const initialState = {
|
||||
isEnabled: false,
|
||||
isGrading: false,
|
||||
oraMetadata: {
|
||||
prompt: '',
|
||||
prompts: [],
|
||||
name: '',
|
||||
type: '',
|
||||
rubricConfig: null,
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('app reducer', () => {
|
||||
});
|
||||
test('populated, but empty ora metadata', () => {
|
||||
const data = initialState.oraMetadata;
|
||||
expect(data.prompt).toEqual('');
|
||||
expect(data.prompts).toEqual([]);
|
||||
expect(data.name).toEqual('');
|
||||
expect(data.type).toEqual('');
|
||||
expect(data.rubricConfig).toEqual(null);
|
||||
|
||||
@@ -33,10 +33,10 @@ export const ora = {
|
||||
*/
|
||||
name: oraMetadataSelector(data => data.name),
|
||||
/**
|
||||
* Returns the ORA Prompt
|
||||
* @return {string} - ORA prompt
|
||||
* Returns the ORA Prompts
|
||||
* @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
|
||||
* @return {string} - ORA type (team vs individual)
|
||||
|
||||
@@ -18,7 +18,10 @@ const testState = {
|
||||
},
|
||||
oraMetadata: {
|
||||
name: 'test-ora-name',
|
||||
prompt: 'test-ora-prompt',
|
||||
prompts: [
|
||||
{ description: 'test-ora-prompt' },
|
||||
{ description: 'test-second-prompt' },
|
||||
],
|
||||
type: 'test-ora-type',
|
||||
fileUploadResponseConfig: 'file-upload-response-config',
|
||||
rubricConfig: {
|
||||
@@ -102,8 +105,8 @@ describe('app selectors unit tests', () => {
|
||||
test('ora.name selector returns name from oraMetadata', () => {
|
||||
testOraSelector(selectors.ora.name, oraMetadata.name);
|
||||
});
|
||||
test('ora.prompt selector returns prompt from oraMetadata', () => {
|
||||
testOraSelector(selectors.ora.prompt, oraMetadata.prompt);
|
||||
test('ora.prompts selector returns prompts from oraMetadata', () => {
|
||||
testOraSelector(selectors.ora.prompts, ['test-ora-prompt', 'test-second-prompt']);
|
||||
});
|
||||
test('ora.type selector returns type from oraMetadata', () => {
|
||||
testOraSelector(selectors.ora.type, oraMetadata.type);
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
/**
|
||||
* get('/api/initialize', { oraLocation })
|
||||
* @return {
|
||||
* oraMetadata: { name, prompt, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig },
|
||||
* oraMetadata: { name, prompts, type ('individual' vs 'team'), rubricConfig, fileUploadResponseConfig },
|
||||
* courseMetadata: { courseOrg, courseName, courseNumber, courseId },
|
||||
* submissions: {
|
||||
* [submissionUUID]: {
|
||||
|
||||
Reference in New Issue
Block a user