Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions src/containers/ResponseDisplay/PromptDisplay.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import React from 'react';
import { Collapsible, Card } from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';

import PropTypes from 'prop-types';
import messages from './messages';

const PromptDisplay = ({
prompt, className, styling, headerTitle,
}) => {
const intl = useIntl();
const msg = intl.formatMessage(messages.promptCollapsibleHeader);
return (
<div className={className}>
<Collapsible
styling={styling}
title={headerTitle ? <h3>{msg}</h3> : msg}
>
{ prompt }
</Collapsible>
</div>
);
};

PromptDisplay.propTypes = {
prompt: PropTypes.string.isRequired,
className: PropTypes.string.isRequired,
styling: PropTypes.string.isRequired,
headerTitle: PropTypes.bool.isRequired,
};

const SinglePromptDisplay = ({ prompt }) => (
<PromptDisplay prompt={prompt} className="prompt-display-single" styling="card-lg" headerTitle />
);

SinglePromptDisplay.propTypes = {
prompt: PropTypes.string.isRequired,
};

const MultiplePromptDisplay = ({ prompt }) => (
<>
<PromptDisplay prompt={prompt} className="prompt-display-multiple" styling="basic" headerTitle={false} />
<Card.Divider />
</>
);

MultiplePromptDisplay.propTypes = {
prompt: PropTypes.string.isRequired,
};

export { SinglePromptDisplay, MultiplePromptDisplay };
12 changes: 12 additions & 0 deletions src/containers/ResponseDisplay/ResponseDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
26 changes: 19 additions & 7 deletions src/containers/ResponseDisplay/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 { SinglePromptDisplay, MultiplePromptDisplay } from './PromptDisplay';
import './ResponseDisplay.scss';

/**
Expand All @@ -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;
}

Expand All @@ -46,15 +46,24 @@ 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));
}

Comment on lines +49 to +53
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: this is technically a sanitizedHtml function. No?

render() {
const { prompts } = this;
const multiPrompt = prompts.length > 1;
return (
<div className="response-display">
{!multiPrompt && <SinglePromptDisplay prompt={prompts[0]} />}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: add a comment as to why this styling changes.

Suggested change
{!multiPrompt && <SinglePromptDisplay prompt={prompts[0]} />}
/* For single prompt ORAs, display the prompt at the top of the page */
{!multiPrompt && <SinglePromptDisplay 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 className="response-display-card" key={index}>
{multiPrompt && <MultiplePromptDisplay prompt={prompts[index]} />}
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
</Card>
))
Expand All @@ -71,6 +80,7 @@ ResponseDisplay.defaultProps = {
},
fileUploadResponseConfig: fileUploadResponseOptions.none,
};

ResponseDisplay.propTypes = {
response: PropTypes.shape({
text: PropTypes.arrayOf(PropTypes.string),
Expand All @@ -83,11 +93,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 = {};
Expand Down
25 changes: 25 additions & 0 deletions src/containers/ResponseDisplay/index.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,17 @@ jest.mock('data/redux', () => ({
app: {
ora: {
fileUploadResponseConfig: jest.fn((state) => state.fileUploadResponseConfig || 'optional'),
prompts: jest.fn((state) => state.prompts || ['prompt']),
},
},
},
}));

jest.mock('./PromptDisplay', () => ({
SinglePromptDisplay: jest.fn(({ prompt }) => (<div data-testid="prompt-single">Prompt: {prompt}</div>)),
MultiplePromptDisplay: jest.fn(({ prompt }) => (<div data-testid="prompt-multiple">Prompt: {prompt}</div>)),
}));

jest.mock('./SubmissionFiles', () => jest.fn(({ files }) => (
<div data-testid="submission-files">Files: {files.length}</div>
)));
Expand Down Expand Up @@ -50,6 +56,7 @@ describe('ResponseDisplay', () => {
],
},
fileUploadResponseConfig: 'optional',
prompts: ['prompt one', 'prompt two'],
};

beforeAll(() => {
Expand Down Expand Up @@ -100,6 +107,18 @@ 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.queryByTestId('prompt-single')).toBeInTheDocument();
expect(screen.queryByTestId('prompt-multiple')).not.toBeInTheDocument();
});

it('displays multiple prompts when there are multiple prompts', () => {
render(<ResponseDisplay {...defaultProps} />);
expect(screen.queryByTestId('prompt-single')).not.toBeInTheDocument();
expect(screen.queryAllByTestId('prompt-multiple')).toHaveLength(2);
});
});

describe('mapStateToProps', () => {
Expand All @@ -109,6 +128,7 @@ describe('ResponseDisplay', () => {
files: ['file1', 'file2'],
},
fileUploadResponseConfig: 'required',
prompts: ['prompt'],
};

it('maps response from grading.selected.response selector', () => {
Expand All @@ -120,5 +140,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));
});
});
});
5 changes: 5 additions & 0 deletions src/containers/ResponseDisplay/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion src/data/redux/app/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const initialState = {
isEnabled: false,
isGrading: false,
oraMetadata: {
prompt: '',
prompts: [],
name: '',
type: '',
rubricConfig: null,
Expand Down
2 changes: 1 addition & 1 deletion src/data/redux/app/reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/data/redux/app/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -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[string]} - ORA prompt
*/
prompt: oraMetadataSelector(data => data.prompt),
prompts: oraMetadataSelector(data => (data.prompts ? data.prompts.map((oraPrompt) => oraPrompt.description) : [])),
Comment on lines +36 to +39
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this require backend changes? If so, can you link them?

/**
* Returns the ORA type
* @return {string} - ORA type (team vs individual)
Expand Down
9 changes: 6 additions & 3 deletions src/data/redux/app/selectors.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/data/services/lms/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]: {
Expand Down