Skip to content

Commit

Permalink
feat: add turnitin displayer to submission preview
Browse files Browse the repository at this point in the history
  • Loading branch information
mariajgrimaldi committed Feb 8, 2024
1 parent 4a708da commit da8683a
Show file tree
Hide file tree
Showing 14 changed files with 229 additions and 4 deletions.
4 changes: 0 additions & 4 deletions .husky/pre-push

This file was deleted.

6 changes: 6 additions & 0 deletions src/containers/ReviewModal/ReviewContent.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Col, Row } from '@edx/paragon';
import { selectors } from 'data/redux';
import { RequestKeys } from 'data/constants/requests';

import TurnitinDisplay from 'containers/TurnitinDisplay';
import ResponseDisplay from 'containers/ResponseDisplay';
import Rubric from 'containers/Rubric';
import ReviewErrors from './ReviewErrors';
Expand All @@ -17,6 +18,11 @@ import ReviewErrors from './ReviewErrors';
export const ReviewContent = ({ isFailed, isLoaded, showRubric }) => (isLoaded || isFailed) && (
<div className="content-block">
<div className="content-wrapper">
<Row className="flex-nowrap my-3">
<Col>
<TurnitinDisplay />
</Col>
</Row>
<ReviewErrors />
{isLoaded && (
<Row className="flex-nowrap m-0">
Expand Down
12 changes: 12 additions & 0 deletions src/containers/TurnitinDisplay/components/FileNameCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import PropTypes from 'prop-types';

export const FileNameCell = ({ value }) => (
<div className="text-truncate">{value.split('.')?.shift()}</div>
);

FileNameCell.propTypes = {
value: PropTypes.string.isRequired,
};

export default FileNameCell;
19 changes: 19 additions & 0 deletions src/containers/TurnitinDisplay/components/HyperlinkCell.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Hyperlink } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';

export const HyperlinkCell = ({ value }) => {
const intl = useIntl();
return (

Check failure on line 9 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 2 spaces but found 3
<Hyperlink destination={value} target="_blank">

Check failure on line 10 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 5 space characters but found 6
{intl.formatMessage(messages.buttonViewerURLTitle)}

Check failure on line 11 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 8 space characters but found 11
</Hyperlink>

Check failure on line 12 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected closing tag to match indentation of opening

Check failure on line 12 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 6 space characters but found 5
)

Check failure on line 13 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Missing semicolon
}

Check failure on line 14 in src/containers/TurnitinDisplay/components/HyperlinkCell.jsx

View workflow job for this annotation

GitHub Actions / tests

Missing semicolon
HyperlinkCell.propTypes = {
value: PropTypes.string.isRequired,
};

export default HyperlinkCell;
41 changes: 41 additions & 0 deletions src/containers/TurnitinDisplay/components/messages.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { defineMessages } from '@edx/frontend-platform/i18n';

const messages = defineMessages({
fileNameTableHeader: {
id: 'ora-grading.TurnitinDisplay.FileNameCell.fileNameTitle',
defaultMessage: 'Responses',
description: 'Title for files attached to the submission',
},
URLTableHeader: {
id: 'ora-grading.TurnitinDisplay.FileNameCell.tableNameHeader',
defaultMessage: 'URL',
description: 'Title for the file name column in the table',
},
buttonViewerURLTitle: {
id: 'ora-grading.TurnitinDisplay.ButtonCell.URLButtonCellTitle',
defaultMessage: 'View in Turnitin',
description: 'Title for the button that opens the viewer in a new tab',
},
similarityReportsTitle: {
id: 'ora-grading.TurnitinDisplay.SimilarityReportsTitle',
defaultMessage: 'Turnitin Similarity Reports',
description: 'Title for the Turnitin Similarity Reports section',
},
noSimilarityReports: {
id: 'ora-grading.TurnitinDisplay.NoSimilarityReports',
defaultMessage: 'No Turnitin Similarity Reports to show',
description: 'Message to display when there are no Turnitin Similarity Reports to show',
},

Check failure on line 28 in src/containers/TurnitinDisplay/components/messages.js

View workflow job for this annotation

GitHub Actions / tests

Expected indentation of 2 spaces but found 4
viewerURLExpired: {
id: 'ora-grading.TurnitinDisplay.ViewerURLExpired',
defaultMessage: 'The Similarity Report URLs have a very short lifespan (less than 1 minute) after which it will no longer be valid. Once a user has been redirected to this URL, they will be given a session that will last for 1 hour. When expired, please refresh the page to get a new URL.',
description: 'Message to display when the viewer URL has expired',
},
viewerURLExpiredTitle: {
id: 'ora-grading.TurnitinDisplay.ViewerURLExpiredTitle',
defaultMessage: 'URLs expire quickly',
description: 'Title for the warning message when the viewer URL has expired',
},
});

export default messages;
101 changes: 101 additions & 0 deletions src/containers/TurnitinDisplay/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';

import {
Card, Collapsible, Icon, DataTable, Alert,
} from '@edx/paragon';
import { ArrowDropDown, ArrowDropUp, WarningFilled } from '@edx/paragon/icons';
import messages from './components/messages';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';

Check failure on line 10 in src/containers/TurnitinDisplay/index.jsx

View workflow job for this annotation

GitHub Actions / tests

`@edx/frontend-platform/i18n` import should occur before import of `./components/messages`

import FileNameCell from './components/FileNameCell';
import HyperlinkCell from './components/HyperlinkCell';


Check failure on line 15 in src/containers/TurnitinDisplay/index.jsx

View workflow job for this annotation

GitHub Actions / tests

More than 1 blank line not allowed
/**
* <TurnitinDisplay />
*/
export const TurnitinDisplay = ({ viewers, intl }) => {
const [isWarningOpen, setIsWarningOpen] = useState(true);
return <Card className="submission-files p-3">
{viewers.length ? (
<>
<Collapsible.Advanced defaultOpen>
<Collapsible.Trigger className="submission-files-title">
<h3>{intl.formatMessage(messages.similarityReportsTitle)}</h3>
<Collapsible.Visible whenClosed>
<Icon src={ArrowDropDown} />
</Collapsible.Visible>
<Collapsible.Visible whenOpen>
<Icon src={ArrowDropUp} />
</Collapsible.Visible>
</Collapsible.Trigger>
<Collapsible.Body className="submission-files-body">
<Alert
variant="warning"
dismissible
icon={WarningFilled}
show={isWarningOpen}
onClose={() => setIsWarningOpen(false)}
>
<Alert.Heading as="h4">{intl.formatMessage(messages.viewerURLExpiredTitle)}</Alert.Heading>
<p className="small mb-0">{intl.formatMessage(messages.viewerURLExpired)}</p>
</Alert>
<div className="submission-files-table">
<DataTable
columns={[
{
Header: intl.formatMessage(messages.fileNameTableHeader),
accessor: "file_name",
Cell: FileNameCell,
},
{
Header: intl.formatMessage(messages.URLTableHeader),
accessor: "url",
Cell: HyperlinkCell,
},
]}
data={viewers}
itemCount={viewers.length}
>
<DataTable.Table />
</DataTable>
</div>
</Collapsible.Body>
</Collapsible.Advanced>
<Card.Footer>
</Card.Footer>
</>
) : (
<div className="submission-files-empty">
<WarningFilled />
<p>{intl.formatMessage(messages.noSimilarityReports)}</p>
</div>
)}
</Card>
};


TurnitinDisplay.defaultProps = {
viewers: [],
};


TurnitinDisplay.propTypes = {
viewers: PropTypes.arrayOf(
PropTypes.shape({
viewer_url: PropTypes.string.isRequired,
}),
),
// injected
intl: intlShape.isRequired,
};

export const mapStateToProps = (state) => ({
viewers: selectors.grading.selected.turnitinViewers(state),
});

export const mapDispatchToProps = {};

export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TurnitinDisplay));
1 change: 1 addition & 0 deletions src/data/constants/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const RequestKeys = StrictDict({
prefetchPrev: 'prefetchPrev',
setLock: 'setLock',
submitGrade: 'submitGrade',
fetchTurnitinViewers: 'fetchTurnitinViewers',
});

export const ErrorCodes = StrictDict({
Expand Down
4 changes: 4 additions & 0 deletions src/data/redux/grading/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,10 @@ const grading = createSlice({
},
};
},
loadTurnitinViewers: (state, { payload }) => ({
...state,
current: { ...state.current, turnitinViewers: payload },
}),
},
});

Expand Down
5 changes: 5 additions & 0 deletions src/data/redux/grading/selectors/selected.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ selected.submissionUUID = createSelector(
*/
selected.gradeStatus = createSelector([simpleSelectors.current], (current) => current.gradeStatus);

selected.turnitinViewers = createSelector(
[simpleSelectors.current],
(current) => current.turnitinViewers,
);

/**
* Returns the lock status for the selected submission
* @return {string} lock status
Expand Down
1 change: 1 addition & 0 deletions src/data/redux/requests/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const initialState = {
[RequestKeys.prefetchNext]: { status: RequestStates.inactive },
[RequestKeys.prefetchPrev]: { status: RequestStates.inactive },
[RequestKeys.submitGrade]: { status: RequestStates.inactive },
[RequestKeys.fetchTurnitinViewers]: { status: RequestStates.inactive },
};

// eslint-disable-next-line no-unused-vars
Expand Down
16 changes: 16 additions & 0 deletions src/data/redux/thunkActions/grading.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const loadSelectionForReview = (submissionUUIDs) => (dispatch) => {
dispatch(actions.grading.updateSelection(submissionUUIDs));
dispatch(actions.app.setShowReview(true));
dispatch(module.loadSubmission());
dispatch(module.loadTurnitinViewers());
};

export const loadSubmission = () => (dispatch, getState) => {
Expand All @@ -58,6 +59,21 @@ export const loadSubmission = () => (dispatch, getState) => {
}));
};

export const loadTurnitinViewers = () => (dispatch, getState) => {
const submissionUUID = selectors.grading.selected.submissionUUID(getState());
dispatch(requests.fetchTurnitinViewers({
submissionUUID, courseId: selectors.app.courseId(getState()),
onSuccess: (response) => {
dispatch(actions.grading.loadTurnitinViewers(response));
},
onFailure: (error) => {
if (error.response.status === ErrorStatuses.notFound) {
dispatch(actions.grading.loadTurnitinViewers([]));
}
}
}));
};

/**
* Start grading the current submission.
* Attempts to lock the submission, and on a success, sets the local grading state to
Expand Down
9 changes: 9 additions & 0 deletions src/data/redux/thunkActions/requests.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ export const fetchSubmission = ({ submissionUUID, ...rest }) => (dispatch) => {
}));
};

export const fetchTurnitinViewers = ({ submissionUUID, courseId, ...rest }) => (dispatch) => {
dispatch(module.networkRequest({
requestKey: RequestKeys.fetchTurnitinViewers,
promise: api.fetchTurnitinViewers(submissionUUID, courseId),
...rest,
}));
}

/**
* Tracked setLock api method. tracked to the `setLock` request key.
* @param {string} submissionUUID - target submission id
Expand Down Expand Up @@ -125,4 +133,5 @@ export default StrictDict({
fetchSubmissionStatus,
setLock,
submitGrade,
fetchTurnitinViewers,
});
12 changes: 12 additions & 0 deletions src/data/services/lms/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ const fetchSubmission = (submissionUUID) => get(
}),
).then(response => response.data);

/**
* get(':courseId/api/v1/viewer-url/:submissionUUID')
* @return {
* url: <string>
* file_name: <string>
* }
*/
const fetchTurnitinViewers = (submissionUUID, courseId) => get(
stringifyUrl(`${urls.fetchTurnitinViewersUrl()}/${courseId}/api/v1/viewer-url/${submissionUUID}/`)
).then(response => response.data);

/**
* get('/api/submission/files', { oraLocation, submissionUUID })
* @return {
Expand Down Expand Up @@ -139,4 +150,5 @@ export default StrictDict({
updateGrade,
unlockSubmission,
batchUnlockSubmissions,
fetchTurnitinViewers,
});
2 changes: 2 additions & 0 deletions src/data/services/lms/urls.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const fetchSubmissionStatusUrl = () => `${baseEsgUrl()}submission/status`;
const fetchSubmissionLockUrl = () => `${baseEsgUrl()}submission/lock`;
const batchUnlockSubmissionsUrl = () => `${baseEsgUrl()}submission/batch/unlock`;
const updateSubmissionGradeUrl = () => `${baseEsgUrl()}submission/grade`;
const fetchTurnitinViewersUrl = () => `${baseUrl()}/platform-plugin-turnitin`;

const course = (courseId) => `${baseUrl()}/courses/${courseId}`;

Expand All @@ -30,6 +31,7 @@ export default StrictDict({
fetchSubmissionLockUrl,
batchUnlockSubmissionsUrl,
updateSubmissionGradeUrl,
fetchTurnitinViewersUrl,
baseUrl,
course,
openResponse,
Expand Down

0 comments on commit da8683a

Please sign in to comment.