From 3d98f998ae16dcfd0eedb7bf0fbbf6baf1df4bc1 Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Wed, 7 Feb 2024 15:22:29 -0400 Subject: [PATCH 1/4] feat: add turnitin displayer to submission preview --- .husky/pre-push | 4 - src/containers/ReviewModal/ReviewContent.jsx | 6 ++ .../components/FileNameCell.jsx | 12 +++ .../components/HyperlinkCell.jsx | 19 ++++ .../TurnitinDisplay/components/messages.js | 41 +++++++ src/containers/TurnitinDisplay/index.jsx | 101 ++++++++++++++++++ src/data/constants/requests.js | 1 + src/data/redux/grading/reducer.js | 4 + src/data/redux/grading/selectors/selected.js | 5 + src/data/redux/requests/reducer.js | 1 + src/data/redux/thunkActions/grading.js | 16 +++ src/data/redux/thunkActions/requests.js | 9 ++ src/data/services/lms/api.js | 12 +++ src/data/services/lms/urls.js | 2 + 14 files changed, 229 insertions(+), 4 deletions(-) delete mode 100755 .husky/pre-push create mode 100644 src/containers/TurnitinDisplay/components/FileNameCell.jsx create mode 100644 src/containers/TurnitinDisplay/components/HyperlinkCell.jsx create mode 100644 src/containers/TurnitinDisplay/components/messages.js create mode 100644 src/containers/TurnitinDisplay/index.jsx diff --git a/.husky/pre-push b/.husky/pre-push deleted file mode 100755 index 20d0d06e5..000000000 --- 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 27e09717d..047b2dd05 100644 --- a/src/containers/ReviewModal/ReviewContent.jsx +++ b/src/containers/ReviewModal/ReviewContent.jsx @@ -7,6 +7,7 @@ import { Col, Row } from '@openedx/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 000000000..e15e78e14 --- /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 000000000..97cec696e --- /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 000000000..64e4a31c8 --- /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 000000000..0f5464812 --- /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 eaa4e90ac..470164cf2 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 ee5ff0f5e..0efb9af22 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 5e5113fde..ee4aeb79a 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 500ee4bb3..b586946fb 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 d26dae918..b7f7cb085 100644 --- a/src/data/redux/thunkActions/grading.js +++ b/src/data/redux/thunkActions/grading.js @@ -39,6 +39,7 @@ export const loadSelectionForReview = (submissionUUIDs) => (dispatch) => { dispatch(actions.grading.updateSelection(submissionUUIDs)); dispatch(actions.app.setShowReview(true)); dispatch(module.loadSubmission()); + dispatch(module.loadTurnitinViewers()); } }; @@ -60,6 +61,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 f1f9f68ca..5ad666c96 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 bc4c41ce3..90f0b7973 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 de95808bf..d4b510959 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, From 46a3289e21a5305814320e8ead7e4a773638801b Mon Sep 17 00:00:00 2001 From: Maria Grimaldi Date: Thu, 8 Feb 2024 16:46:17 -0400 Subject: [PATCH 2/4] fix: import selectors from data/redux --- src/containers/TurnitinDisplay/index.jsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/containers/TurnitinDisplay/index.jsx b/src/containers/TurnitinDisplay/index.jsx index 0f5464812..4a4cf47d3 100644 --- a/src/containers/TurnitinDisplay/index.jsx +++ b/src/containers/TurnitinDisplay/index.jsx @@ -11,6 +11,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import FileNameCell from './components/FileNameCell'; import HyperlinkCell from './components/HyperlinkCell'; +import { selectors } from 'data/redux'; /** From 64dbd8943044a248cfcdf4b1137a79f188b28217 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Wed, 1 May 2024 11:50:37 -0500 Subject: [PATCH 3/4] chore: fix linter checks --- .../components/HyperlinkCell.jsx | 14 +- .../TurnitinDisplay/components/messages.js | 2 +- src/containers/TurnitinDisplay/index.jsx | 140 ++++++++++-------- src/data/redux/thunkActions/grading.js | 5 +- src/data/redux/thunkActions/requests.js | 2 +- src/data/services/lms/api.js | 2 +- 6 files changed, 89 insertions(+), 76 deletions(-) diff --git a/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx b/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx index 97cec696e..c20af8b4c 100644 --- a/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx +++ b/src/containers/TurnitinDisplay/components/HyperlinkCell.jsx @@ -1,17 +1,17 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Hyperlink } from '@edx/paragon'; +import { Hyperlink } from '@openedx/paragon'; import { useIntl } from '@edx/frontend-platform/i18n'; import messages from './messages'; export const HyperlinkCell = ({ value }) => { const intl = useIntl(); - return ( - - {intl.formatMessage(messages.buttonViewerURLTitle)} - - ) -} + return ( + + {intl.formatMessage(messages.buttonViewerURLTitle)} + + ); +}; HyperlinkCell.propTypes = { value: PropTypes.string.isRequired, }; diff --git a/src/containers/TurnitinDisplay/components/messages.js b/src/containers/TurnitinDisplay/components/messages.js index 64e4a31c8..3a6b1d698 100644 --- a/src/containers/TurnitinDisplay/components/messages.js +++ b/src/containers/TurnitinDisplay/components/messages.js @@ -25,7 +25,7 @@ const messages = defineMessages({ 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.', diff --git a/src/containers/TurnitinDisplay/index.jsx b/src/containers/TurnitinDisplay/index.jsx index 4a4cf47d3..4d53e8300 100644 --- a/src/containers/TurnitinDisplay/index.jsx +++ b/src/containers/TurnitinDisplay/index.jsx @@ -3,86 +3,96 @@ 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'; + Card, + Collapsible, + Icon, + DataTable, + Alert, +} from '@openedx/paragon'; +import { + ArrowDropDown, + ArrowDropUp, + WarningFilled, +} from '@openedx/paragon/icons'; +import { selectors } from 'data/redux'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; +import messages from './components/messages'; import FileNameCell from './components/FileNameCell'; import HyperlinkCell from './components/HyperlinkCell'; -import { selectors } from 'data/redux'; - /** * */ 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)}

-
-
- + {viewers.length ? ( + <> + + +

{intl.formatMessage(messages.similarityReportsTitle)}

+ + + + + + +
+ + setIsWarningOpen(false)} > - -
-
-
-
- - - - ) : ( -
- -

{intl.formatMessage(messages.noSimilarityReports)}

-
- )} -
+ + {intl.formatMessage(messages.viewerURLExpiredTitle)} + +

+ {intl.formatMessage(messages.viewerURLExpired)} +

+ +
+ + + +
+ + + {} + + ) : ( +
+ +

{intl.formatMessage(messages.noSimilarityReports)}

+
+ )} + + ); }; - TurnitinDisplay.defaultProps = { viewers: [], }; - TurnitinDisplay.propTypes = { viewers: PropTypes.arrayOf( PropTypes.shape({ @@ -99,4 +109,6 @@ export const mapStateToProps = (state) => ({ export const mapDispatchToProps = {}; -export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(TurnitinDisplay)); +export default injectIntl( + connect(mapStateToProps, mapDispatchToProps)(TurnitinDisplay), +); diff --git a/src/data/redux/thunkActions/grading.js b/src/data/redux/thunkActions/grading.js index b7f7cb085..98c096a9a 100644 --- a/src/data/redux/thunkActions/grading.js +++ b/src/data/redux/thunkActions/grading.js @@ -64,7 +64,8 @@ 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()), + submissionUUID, + courseId: selectors.app.courseId(getState()), onSuccess: (response) => { dispatch(actions.grading.loadTurnitinViewers(response)); }, @@ -72,7 +73,7 @@ export const loadTurnitinViewers = () => (dispatch, getState) => { if (error.response.status === ErrorStatuses.notFound) { dispatch(actions.grading.loadTurnitinViewers([])); } - } + }, })); }; diff --git a/src/data/redux/thunkActions/requests.js b/src/data/redux/thunkActions/requests.js index 5ad666c96..5b6b23f4f 100644 --- a/src/data/redux/thunkActions/requests.js +++ b/src/data/redux/thunkActions/requests.js @@ -84,7 +84,7 @@ export const fetchTurnitinViewers = ({ submissionUUID, courseId, ...rest }) => ( promise: api.fetchTurnitinViewers(submissionUUID, courseId), ...rest, })); -} +}; /** * Tracked setLock api method. tracked to the `setLock` request key. diff --git a/src/data/services/lms/api.js b/src/data/services/lms/api.js index 90f0b7973..39bae7c47 100644 --- a/src/data/services/lms/api.js +++ b/src/data/services/lms/api.js @@ -62,7 +62,7 @@ const fetchSubmission = (submissionUUID) => get( * } */ const fetchTurnitinViewers = (submissionUUID, courseId) => get( - stringifyUrl(`${urls.fetchTurnitinViewersUrl()}/${courseId}/api/v1/viewer-url/${submissionUUID}/`) + stringifyUrl(`${urls.fetchTurnitinViewersUrl()}/${courseId}/api/v1/viewer-url/${submissionUUID}/`), ).then(response => response.data); /** From 4b5796987dd89a18cb24ec0240fedb047d8aa8a4 Mon Sep 17 00:00:00 2001 From: Bryann Valderrama Date: Thu, 2 May 2024 13:42:54 -0500 Subject: [PATCH 4/4] chore: update snapshots --- .../__snapshots__/ReviewContent.test.jsx.snap | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap b/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap index 4a57d2bfd..f1008157e 100644 --- a/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap +++ b/src/containers/ReviewModal/__snapshots__/ReviewContent.test.jsx.snap @@ -7,6 +7,13 @@ exports[`ReviewContent component component render tests snapshot: failed, showRu
+ + + + +
@@ -19,6 +26,13 @@ exports[`ReviewContent component component render tests snapshot: hide rubric 1`
+ + + + + + + + + +