diff --git a/.husky/pre-push b/.husky/pre-push
deleted file mode 100755
index 20d0d06e..00000000
--- a/.husky/pre-push
+++ /dev/null
@@ -1,4 +0,0 @@
-#!/bin/sh
-. "$(dirname "$0")/_/husky.sh"
-
-npm run lint
diff --git a/src/containers/ReviewModal/ReviewContent.jsx b/src/containers/ReviewModal/ReviewContent.jsx
index b444236f..137b135d 100644
--- a/src/containers/ReviewModal/ReviewContent.jsx
+++ b/src/containers/ReviewModal/ReviewContent.jsx
@@ -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';
@@ -17,6 +18,11 @@ import ReviewErrors from './ReviewErrors';
export const ReviewContent = ({ isFailed, isLoaded, showRubric }) => (isLoaded || isFailed) && (
+
+
+
+
+
{isLoaded && (
diff --git a/src/containers/TurnitinDisplay/components/FileNameCell.jsx b/src/containers/TurnitinDisplay/components/FileNameCell.jsx
new file mode 100644
index 00000000..e15e78e1
--- /dev/null
+++ b/src/containers/TurnitinDisplay/components/FileNameCell.jsx
@@ -0,0 +1,12 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+export const FileNameCell = ({ value }) => (
+ {value.split('.')?.shift()}
+);
+
+FileNameCell.propTypes = {
+ value: PropTypes.string.isRequired,
+};
+
+export default FileNameCell;
diff --git a/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx b/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx
new file mode 100644
index 00000000..97cec696
--- /dev/null
+++ b/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx
@@ -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 (
+
+ {intl.formatMessage(messages.buttonViewerURLTitle)}
+
+ )
+}
+HyperlinkCell.propTypes = {
+ value: PropTypes.string.isRequired,
+};
+
+export default HyperlinkCell;
diff --git a/src/containers/TurnitinDisplay/components/messages.js b/src/containers/TurnitinDisplay/components/messages.js
new file mode 100644
index 00000000..64e4a31c
--- /dev/null
+++ b/src/containers/TurnitinDisplay/components/messages.js
@@ -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',
+ },
+ 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;
diff --git a/src/containers/TurnitinDisplay/index.jsx b/src/containers/TurnitinDisplay/index.jsx
new file mode 100644
index 00000000..0f546481
--- /dev/null
+++ b/src/containers/TurnitinDisplay/index.jsx
@@ -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';
+
+import FileNameCell from './components/FileNameCell';
+import HyperlinkCell from './components/HyperlinkCell';
+
+
+/**
+ *
+ */
+export const TurnitinDisplay = ({ viewers, intl }) => {
+ const [isWarningOpen, setIsWarningOpen] = useState(true);
+ return
+ {viewers.length ? (
+ <>
+
+
+ {intl.formatMessage(messages.similarityReportsTitle)}
+
+
+
+
+
+
+
+
+ setIsWarningOpen(false)}
+ >
+ {intl.formatMessage(messages.viewerURLExpiredTitle)}
+ {intl.formatMessage(messages.viewerURLExpired)}
+
+
+
+
+
+
+
+
+
+
+ >
+ ) : (
+
+
+
{intl.formatMessage(messages.noSimilarityReports)}
+
+ )}
+
+};
+
+
+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));
diff --git a/src/data/constants/requests.js b/src/data/constants/requests.js
index eaa4e90a..470164cf 100644
--- a/src/data/constants/requests.js
+++ b/src/data/constants/requests.js
@@ -17,6 +17,7 @@ export const RequestKeys = StrictDict({
prefetchPrev: 'prefetchPrev',
setLock: 'setLock',
submitGrade: 'submitGrade',
+ fetchTurnitinViewers: 'fetchTurnitinViewers',
});
export const ErrorCodes = StrictDict({
diff --git a/src/data/redux/grading/reducer.js b/src/data/redux/grading/reducer.js
index ee5ff0f5..0efb9af2 100644
--- a/src/data/redux/grading/reducer.js
+++ b/src/data/redux/grading/reducer.js
@@ -199,6 +199,10 @@ const grading = createSlice({
},
};
},
+ loadTurnitinViewers: (state, { payload }) => ({
+ ...state,
+ current: { ...state.current, turnitinViewers: payload },
+ }),
},
});
diff --git a/src/data/redux/grading/selectors/selected.js b/src/data/redux/grading/selectors/selected.js
index 5e5113fd..ee4aeb79 100644
--- a/src/data/redux/grading/selectors/selected.js
+++ b/src/data/redux/grading/selectors/selected.js
@@ -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
diff --git a/src/data/redux/requests/reducer.js b/src/data/redux/requests/reducer.js
index 500ee4bb..b586946f 100644
--- a/src/data/redux/requests/reducer.js
+++ b/src/data/redux/requests/reducer.js
@@ -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
diff --git a/src/data/redux/thunkActions/grading.js b/src/data/redux/thunkActions/grading.js
index e0b7e7be..59eace20 100644
--- a/src/data/redux/thunkActions/grading.js
+++ b/src/data/redux/thunkActions/grading.js
@@ -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) => {
@@ -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
diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js
index f1f9f68c..5ad666c9 100644
--- a/src/data/redux/thunkActions/requests.js
+++ b/src/data/redux/thunkActions/requests.js
@@ -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
@@ -125,4 +133,5 @@ export default StrictDict({
fetchSubmissionStatus,
setLock,
submitGrade,
+ fetchTurnitinViewers,
});
diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js
index bc4c41ce..90f0b797 100644
--- a/src/data/services/lms/api.js
+++ b/src/data/services/lms/api.js
@@ -54,6 +54,17 @@ const fetchSubmission = (submissionUUID) => get(
}),
).then(response => response.data);
+/**
+ * get(':courseId/api/v1/viewer-url/:submissionUUID')
+ * @return {
+ * url:
+ * file_name:
+ * }
+ */
+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 {
@@ -139,4 +150,5 @@ export default StrictDict({
updateGrade,
unlockSubmission,
batchUnlockSubmissions,
+ fetchTurnitinViewers,
});
diff --git a/src/data/services/lms/urls.js b/src/data/services/lms/urls.js
index de95808b..d4b51095 100644
--- a/src/data/services/lms/urls.js
+++ b/src/data/services/lms/urls.js
@@ -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}`;
@@ -30,6 +31,7 @@ export default StrictDict({
fetchSubmissionLockUrl,
batchUnlockSubmissionsUrl,
updateSubmissionGradeUrl,
+ fetchTurnitinViewersUrl,
baseUrl,
course,
openResponse,