diff --git a/src/components/Assignments/Assignment/AssignmentDetails/AssignmentDetails.js b/src/components/Assignments/Assignment/AssignmentDetails/AssignmentDetails.js index 82f9119c2..b56a230d1 100644 --- a/src/components/Assignments/Assignment/AssignmentDetails/AssignmentDetails.js +++ b/src/components/Assignments/Assignment/AssignmentDetails/AssignmentDetails.js @@ -1,5 +1,6 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; import { Table, Modal } from 'react-bootstrap'; import { FormattedMessage } from 'react-intl'; @@ -34,6 +35,7 @@ const AssignmentDetails = ({ maxPointsBeforeSecondDeadline, isBonus, runtimeEnvironments, + assignmentSolver = null, canSubmit, pointsPercentualThreshold, isPublic, @@ -56,6 +58,8 @@ const AssignmentDetails = ({ maxPointsBeforeSecondDeadline, }); + const lastAttemptIndex = assignmentSolver && assignmentSolver.get('lastAttemptIndex'); + return ( } @@ -249,7 +253,7 @@ const AssignmentDetails = ({ - {isStudent && canSubmit.canSubmit ? : } + {isStudent && canSubmit.canSubmit ? : } : @@ -263,8 +267,19 @@ const AssignmentDetails = ({ {isStudent ? ( <> - {canSubmit.submittedCount} + {lastAttemptIndex || canSubmit.submittedCount} {submissionsCountLimit !== null && ` / ${submissionsCountLimit}`} + {lastAttemptIndex && lastAttemptIndex > canSubmit.submittedCount && ( + + ( + + ) + + )} ) : ( <>{submissionsCountLimit === null ? '-' : submissionsCountLimit} @@ -274,7 +289,7 @@ const AssignmentDetails = ({ - + : @@ -358,6 +373,7 @@ AssignmentDetails.propTypes = { permissionHints: PropTypes.object.isRequired, isStudent: PropTypes.bool.isRequired, className: PropTypes.string, + assignmentSolver: ImmutablePropTypes.map, }; export default AssignmentDetails; diff --git a/src/components/Assignments/SolutionsTable/SolutionsTable.js b/src/components/Assignments/SolutionsTable/SolutionsTable.js index 0e99baa4d..4e4b33e7c 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTable.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTable.js @@ -18,10 +18,13 @@ const SolutionsTable = ({ noteMaxlen = null, compact = false, selected = null, + assignmentSolver = null, + assignmentSolversLoading = false, }) => ( + )} @@ -100,6 +135,8 @@ SolutionsTable.propTypes = { noteMaxlen: PropTypes.number, compact: PropTypes.bool, selected: PropTypes.string, + assignmentSolver: ImmutablePropTypes.map, + assignmentSolversLoading: PropTypes.bool, }; export default SolutionsTable; diff --git a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js index f4dd4ffdd..1add61936 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js @@ -24,6 +24,7 @@ const showScoreAndPoints = status => status === 'done' || status === 'failed'; const SolutionsTableRow = ({ id, + attemptIndex, assignmentId, groupId, status = null, @@ -60,6 +61,8 @@ const SolutionsTableRow = ({ return ( + + @@ -338,6 +344,7 @@ class SolutionDetail extends Component { SolutionDetail.propTypes = { solution: PropTypes.shape({ id: PropTypes.string.isRequired, + attemptIndex: PropTypes.number.isRequired, note: PropTypes.string, lastSubmission: PropTypes.shape({ id: PropTypes.string.isRequired }), createdAt: PropTypes.number.isRequired, @@ -354,6 +361,8 @@ SolutionDetail.propTypes = { files: ImmutablePropTypes.map, download: PropTypes.func, otherSolutions: ImmutablePropTypes.list.isRequired, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, assignment: PropTypes.object.isRequired, evaluations: PropTypes.object.isRequired, runtimeEnvironments: PropTypes.array, diff --git a/src/components/Solutions/SolutionStatus/SolutionStatus.js b/src/components/Solutions/SolutionStatus/SolutionStatus.js index c5b5e3a4b..e489c4572 100644 --- a/src/components/Solutions/SolutionStatus/SolutionStatus.js +++ b/src/components/Solutions/SolutionStatus/SolutionStatus.js @@ -27,6 +27,7 @@ import Icon, { FailureIcon, CodeIcon, LinkIcon, + LoadingIcon, WarningIcon, } from '../../icons'; import AssignmentDeadlinesGraph from '../../Assignments/Assignment/AssignmentDeadlinesGraph'; @@ -69,6 +70,7 @@ class SolutionStatus extends Component { render() { const { id, + attemptIndex, otherSolutions, assignment: { id: assignmentId, @@ -96,6 +98,8 @@ class SolutionStatus extends Component { actualPoints, overriddenPoints = null, editNote = null, + assignmentSolversLoading, + assignmentSolverSelector, links: { SOLUTION_DETAIL_URI_FACTORY }, } = this.props; @@ -103,6 +107,9 @@ class SolutionStatus extends Component { const environment = runtimeEnvironments && runtimeEnvironmentId && runtimeEnvironments.find(({ id }) => id === runtimeEnvironmentId); + const assignmentSolver = assignmentSolverSelector && assignmentSolverSelector(assignmentId, userId); + const lastAttemptIndex = assignmentSolver && assignmentSolver.get('lastAttemptIndex'); + return ( <> 0 ? ( note ) : ( - + )} @@ -348,14 +355,18 @@ class SolutionStatus extends Component { :
@@ -41,12 +44,44 @@ const SolutionsTable = ({ - {solutions.size > 5 && ( - + {assignmentSolversLoading ? ( + + ) : ( + <> + {assignmentSolver && + (assignmentSolver.get('lastAttemptIndex') > 5 || + assignmentSolver.get('lastAttemptIndex') > solutions.size) && ( + <> + {!compact && ( + + )} + + {assignmentSolver.get('lastAttemptIndex') > solutions.size && ( + + {!compact && <>  }( + + ) + + )} + + )} + + {!compact && !assignmentSolver && solutions.size > 5 && ( + + )} + )}
{attemptIndex}. id === runtimeEnvironmentId) } otherSolutions={otherSolutions} + assignmentSolversLoading={assignmentSolversLoading} + assignmentSolverSelector={assignmentSolverSelector} />
- + {!lastAttemptIndex ? ( + + ) : ( + + )} {otherSolutions && otherSolutions.size > 1 && ( @@ -538,6 +551,7 @@ class SolutionStatus extends Component { SolutionStatus.propTypes = { id: PropTypes.string.isRequired, + attemptIndex: PropTypes.number.isRequired, otherSolutions: ImmutablePropTypes.list.isRequired, assignment: PropTypes.shape({ id: PropTypes.string.isRequired, @@ -565,6 +579,8 @@ SolutionStatus.propTypes = { actualPoints: PropTypes.number, overriddenPoints: PropTypes.number, editNote: PropTypes.func, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, links: PropTypes.object.isRequired, }; diff --git a/src/helpers/common.js b/src/helpers/common.js index ca80d0c05..df6d4e65e 100644 --- a/src/helpers/common.js +++ b/src/helpers/common.js @@ -145,7 +145,7 @@ const idSelector = obj => obj.id; * @param {Function} predicate called on every entry, returns true should the entry remain * @returns {Object} clone of obj with entries filtered out */ -export const objectFilter = (obj, predicate) => { +export const objectFilter = (obj, predicate = val => Boolean(val)) => { const res = {}; Object.keys(obj) .filter(key => predicate(obj[key], key)) diff --git a/src/locales/cs.json b/src/locales/cs.json index 3f2db5316..02d3f4b15 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -109,6 +109,7 @@ "app.assignment.solutionFilesLimitCount": "{count} {count, plural, one {soubor} =2 {soubory} =3 {soubory} =4 {soubory} other {souborů}}", "app.assignment.solutionFilesLimitExplanation": "Omezení se uplatňují na počet odevzdaných souborů a jejich celkovou velikost.", "app.assignment.solutionFilesLimitSize": "{size} KiB {count, plural, one {} other {celkem}}", + "app.assignment.submissionCountLimitIncreasedByDeletion": "+{count} {count, plural, one {pokus vytvořen} =2 {pokusy vytvořeny} =3 {pokusy vytvořeny} =4 {pokusy vytvořeny} other {pokusů vytvořeno}} smazáním řešení", "app.assignment.submissionsCountLimit": "Počet pokusů", "app.assignment.submissionsCountLimitExplanation": "Maximální počet odevzdaných řešení této úlohy od jednoho studenta. Vyučující můž udělit další pokusy tak, že smaže starší odevzdaná řešení.", "app.assignment.syncAttachmentFiles": "Přiložené soubory", @@ -1586,6 +1587,8 @@ "app.solutionFiles.title": "Odevzdané soubory", "app.solutionFiles.total": "Celkem:", "app.solutionsTable.assignment": "Úloha", + "app.solutionsTable.attemptsCount": "Odevzdaných řešení: {count}", + "app.solutionsTable.attemptsDeleted": "{deleted} {deleted, plural, =2 {smazána} =3 {smazána} =4 {smazána} other {smazáno}}", "app.solutionsTable.commentsIcon.count": "Počet komentářů: {count}", "app.solutionsTable.commentsIcon.last": "Poslední komentář: {last}", "app.solutionsTable.environment": "Cílový jazyk", diff --git a/src/locales/en.json b/src/locales/en.json index 5f00ebaa1..3d299f8a9 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -109,6 +109,7 @@ "app.assignment.solutionFilesLimitCount": "{count} {count, plural, one {file} other {files}}", "app.assignment.solutionFilesLimitExplanation": "The restrictions may limit maximal number of submitted files and their total size.", "app.assignment.solutionFilesLimitSize": "{size} KiB {count, plural, one {} other {total}}", + "app.assignment.submissionCountLimitIncreasedByDeletion": "+{count} {count, plural, one {attempt} other {attempts}} added by deleted solutions", "app.assignment.submissionsCountLimit": "Submission attempts", "app.assignment.submissionsCountLimitExplanation": "Maximal number of solutions logged by one student for this assignment. The teacher may choose to grant additional attempts by deleting old solutions.", "app.assignment.syncAttachmentFiles": "Text attachment files", @@ -1586,6 +1587,8 @@ "app.solutionFiles.title": "Submitted Files", "app.solutionFiles.total": "Total:", "app.solutionsTable.assignment": "Assignment", + "app.solutionsTable.attemptsCount": "Solutions submitted: {count}", + "app.solutionsTable.attemptsDeleted": "{deleted} deleted", "app.solutionsTable.commentsIcon.count": "Total Comments: {count}", "app.solutionsTable.commentsIcon.last": "Last Comment: {last}", "app.solutionsTable.environment": "Target language", diff --git a/src/pages/Assignment/Assignment.js b/src/pages/Assignment/Assignment.js index 10c797c7d..9168fd63f 100644 --- a/src/pages/Assignment/Assignment.js +++ b/src/pages/Assignment/Assignment.js @@ -15,7 +15,7 @@ import { submitAssignmentSolution as submitSolution, presubmitAssignmentSolution as presubmitSolution, } from '../../redux/modules/submission'; -import { fetchUsersSolutions } from '../../redux/modules/solutions'; +import { fetchUsersSolutions, fetchAssignmentSolversIfNeeded } from '../../redux/modules/solutions'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; import { @@ -26,7 +26,11 @@ import { import { canSubmitSolution } from '../../redux/selectors/canSubmit'; import { isSubmitting } from '../../redux/selectors/submission'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; -import { fetchManyUserSolutionsStatus } from '../../redux/selectors/solutions'; +import { + fetchManyUserSolutionsStatus, + isAssignmentSolversLoading, + getAssignmentSolverSelector, +} from '../../redux/selectors/solutions'; import { loggedUserIsStudentOfSelector, loggedUserIsObserverOfSelector, @@ -58,6 +62,7 @@ class Assignment extends Component { dispatch(fetchRuntimeEnvironments()), dispatch(canSubmit(assignmentId)), dispatch(fetchUsersSolutions(userId, assignmentId)), + dispatch(fetchAssignmentSolversIfNeeded({ assignmentId, userId })), ]); componentDidMount() { @@ -90,6 +95,8 @@ class Assignment extends Component { exerciseSync, solutions, fetchManyStatus, + assignmentSolversLoading, + assignmentSolverSelector, } = this.props; return ( @@ -141,6 +148,7 @@ class Assignment extends Component { {...assignment} canSubmit={canSubmitObj} runtimeEnvironments={runtimes} + assignmentSolver={assignmentSolverSelector(assignment.id, userId || loggedInUserId)} isStudent={isStudentOf(assignment.groupId)} className="d-flex d-xl-none" /> @@ -156,6 +164,7 @@ class Assignment extends Component { {...assignment} canSubmit={canSubmitObj} runtimeEnvironments={runtimes} + assignmentSolver={assignmentSolverSelector(assignment.id, userId || loggedInUserId)} isStudent={isStudentOf(assignment.groupId)} className="d-none d-xl-flex" /> @@ -202,6 +211,8 @@ class Assignment extends Component { runtimeEnvironments={runtimes} noteMaxlen={64} compact + assignmentSolversLoading={assignmentSolversLoading} + assignmentSolver={assignmentSolverSelector(assignment.id, userId || loggedInUserId)} /> )} @@ -256,6 +267,8 @@ Assignment.propTypes = { exerciseSync: PropTypes.func.isRequired, solutions: ImmutablePropTypes.list.isRequired, fetchManyStatus: PropTypes.string, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, }; export default connect( @@ -281,6 +294,8 @@ export default connect( canSubmit: canSubmitSolution(assignmentId)(state), solutions: getUserSolutionsSortedData(state)(userId || loggedInUserId, assignmentId), fetchManyStatus: fetchManyUserSolutionsStatus(state)(userId || loggedInUserId, assignmentId), + assignmentSolversLoading: isAssignmentSolversLoading(state), + assignmentSolverSelector: getAssignmentSolverSelector(state), }; }, ( diff --git a/src/pages/AssignmentStats/AssignmentStats.js b/src/pages/AssignmentStats/AssignmentStats.js index f82505c03..69b11dd26 100644 --- a/src/pages/AssignmentStats/AssignmentStats.js +++ b/src/pages/AssignmentStats/AssignmentStats.js @@ -37,7 +37,7 @@ import { fetchByIds } from '../../redux/modules/users'; import { fetchAssignmentIfNeeded, downloadBestSolutionsArchive } from '../../redux/modules/assignments'; import { fetchGroupIfNeeded } from '../../redux/modules/groups'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; -import { fetchAssignmentSolutions } from '../../redux/modules/solutions'; +import { fetchAssignmentSolutions, fetchAssignmentSolversIfNeeded } from '../../redux/modules/solutions'; import { usersSelector } from '../../redux/selectors/users'; import { groupSelector } from '../../redux/selectors/groups'; import { studentsIdsOfGroup } from '../../redux/selectors/usersGroups'; @@ -48,7 +48,11 @@ import { getAssignmentSolutions, } from '../../redux/selectors/assignments'; import { loggedInUserIdSelector } from '../../redux/selectors/auth'; -import { fetchManyAssignmentSolutionsStatus } from '../../redux/selectors/solutions'; +import { + fetchManyAssignmentSolutionsStatus, + isAssignmentSolversLoading, + getAssignmentSolverSelector, +} from '../../redux/selectors/solutions'; import { isReady, getJsData, getId } from '../../redux/helpers/resourceManager'; import { storageGetItem, storageSetItem } from '../../helpers/localStorage'; @@ -243,6 +247,7 @@ class AssignmentStats extends Component { ) ), dispatch(fetchRuntimeEnvironments()), + dispatch(fetchAssignmentSolversIfNeeded({ assignmentId })), dispatch(fetchAssignmentSolutions(assignmentId)), ]); @@ -297,6 +302,8 @@ class AssignmentStats extends Component { assignmentSolutions, downloadBestSolutionsArchive, fetchManyStatus, + assignmentSolversLoading, + assignmentSolverSelector, intl: { locale }, links, } = this.props; @@ -434,6 +441,8 @@ class AssignmentStats extends Component { groupId={assignment.groupId} runtimeEnvironments={runtimes} noteMaxlen={160} + assignmentSolversLoading={assignmentSolversLoading} + assignmentSolver={assignmentSolverSelector(assignmentId, user.id)} /> @@ -494,6 +503,8 @@ AssignmentStats.propTypes = { loadAsync: PropTypes.func.isRequired, downloadBestSolutionsArchive: PropTypes.func.isRequired, fetchManyStatus: PropTypes.string, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, intl: PropTypes.object, links: PropTypes.object.isRequired, }; @@ -523,6 +534,8 @@ export default withLinks( getGroup: id => groupSelector(state, id), runtimeEnvironments: assignmentEnvironmentsSelector(state)(assignmentId), fetchManyStatus: fetchManyAssignmentSolutionsStatus(assignmentId)(state), + assignmentSolversLoading: isAssignmentSolversLoading(state), + assignmentSolverSelector: getAssignmentSolverSelector(state), }; }, ( diff --git a/src/pages/GroupUserSolutions/GroupUserSolutions.js b/src/pages/GroupUserSolutions/GroupUserSolutions.js index c05aa3bb0..5a4f73533 100644 --- a/src/pages/GroupUserSolutions/GroupUserSolutions.js +++ b/src/pages/GroupUserSolutions/GroupUserSolutions.js @@ -32,15 +32,18 @@ import { fetchUserIfNeeded } from '../../redux/modules/users'; import { fetchAssignmentsForGroup } from '../../redux/modules/assignments'; import { fetchGroupIfNeeded } from '../../redux/modules/groups'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; -import { fetchGroupStudentsSolutions } from '../../redux/modules/solutions'; +import { fetchGroupStudentsSolutions, fetchAssignmentSolversIfNeeded } from '../../redux/modules/solutions'; import { groupSelector, groupsAssignmentsSelector, groupDataAccessorSelector } from '../../redux/selectors/groups'; import { assignmentEnvironmentsSelector, getUserSolutions, getUserSolutionsSortedData, } from '../../redux/selectors/assignments'; -import { loggedInUserIdSelector } from '../../redux/selectors/auth'; -import { fetchManyGroupStudentsSolutionsStatus } from '../../redux/selectors/solutions'; +import { + fetchManyGroupStudentsSolutionsStatus, + isAssignmentSolversLoading, + getAssignmentSolverSelector, +} from '../../redux/selectors/solutions'; import { runtimeEnvironmentSelector, fetchRuntimeEnvironmentsStatus } from '../../redux/selectors/runtimeEnvironments'; import { getJsData } from '../../redux/helpers/resourceManager'; import { compareAssignmentsReverted } from '../../components/helpers/assignments'; @@ -262,7 +265,11 @@ class GroupUserSolutions extends Component { dispatch(fetchGroupIfNeeded(groupId)).then(({ value: group }) => Promise.all( hasPermissions(group, 'viewAssignments') - ? [dispatch(fetchAssignmentsForGroup(groupId)), dispatch(fetchGroupStudentsSolutions(groupId, userId))] + ? [ + dispatch(fetchAssignmentsForGroup(groupId)), + dispatch(fetchGroupStudentsSolutions(groupId, userId)), + dispatch(fetchAssignmentSolversIfNeeded({ groupId, userId })), + ] : [] ) ), @@ -306,6 +313,8 @@ class GroupUserSolutions extends Component { getRuntime, fetchSolutionsStatus, fetchRuntimesStatus, + assignmentSolversLoading, + assignmentSolverSelector, intl: { locale }, links, } = this.props; @@ -406,6 +415,8 @@ class GroupUserSolutions extends Component { groupId={groupId} runtimeEnvironments={assignmentEnvironmentsSelector(assignment.id).map(getJsData)} noteMaxlen={160} + assignmentSolversLoading={assignmentSolversLoading} + assignmentSolver={assignmentSolverSelector(assignment.id, userId)} /> @@ -468,6 +479,8 @@ GroupUserSolutions.propTypes = { getAssignmentSolutions: PropTypes.func.isRequired, getAssignmentSolutionsSorted: PropTypes.func.isRequired, getRuntime: PropTypes.func, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, loadAsync: PropTypes.func.isRequired, intl: PropTypes.object, links: PropTypes.object.isRequired, @@ -495,7 +508,8 @@ export default withLinks( getAssignmentSolutions: assignmentId => getUserSolutions(state)(userId, assignmentId), getAssignmentSolutionsSorted: assignmentId => getUserSolutionsSortedData(state)(userId, assignmentId), getRuntime: runtimeEnvironmentSelector(state), - loggedUserId: loggedInUserIdSelector(state), + assignmentSolversLoading: isAssignmentSolversLoading(state), + assignmentSolverSelector: getAssignmentSolverSelector(state), }; }, ( diff --git a/src/pages/Solution/Solution.js b/src/pages/Solution/Solution.js index 3b696d8e6..36ef5619f 100644 --- a/src/pages/Solution/Solution.js +++ b/src/pages/Solution/Solution.js @@ -17,7 +17,13 @@ import { TheButtonGroup } from '../../components/widgets/TheButton'; import { fetchRuntimeEnvironments } from '../../redux/modules/runtimeEnvironments'; import { fetchAssignmentIfNeeded } from '../../redux/modules/assignments'; -import { fetchSolution, fetchSolutionIfNeeded, fetchUsersSolutions, setNote } from '../../redux/modules/solutions'; +import { + fetchSolution, + fetchSolutionIfNeeded, + fetchUsersSolutions, + setNote, + fetchAssignmentSolversIfNeeded, +} from '../../redux/modules/solutions'; import { fetchAssignmentSolutionFilesIfNeeded } from '../../redux/modules/solutionFiles'; import { download } from '../../redux/modules/files'; import { @@ -25,7 +31,7 @@ import { deleteSubmissionEvaluation, } from '../../redux/modules/submissionEvaluations'; import { fetchAssignmentSubmissionScoreConfigIfNeeded } from '../../redux/modules/exerciseScoreConfig'; -import { getSolution } from '../../redux/selectors/solutions'; +import { getSolution, isAssignmentSolversLoading, getAssignmentSolverSelector } from '../../redux/selectors/solutions'; import { getSolutionFiles } from '../../redux/selectors/solutionFiles'; import { getAssignment, @@ -50,7 +56,12 @@ class Solution extends Component { dispatch(fetchRuntimeEnvironments()), dispatch(fetchSolutionIfNeeded(solutionId)) .then(res => res.value) - .then(solution => dispatch(fetchUsersSolutions(solution.authorId, assignmentId))), + .then(solution => + Promise.all([ + dispatch(fetchUsersSolutions(solution.authorId, assignmentId)), + dispatch(fetchAssignmentSolversIfNeeded({ assignmentId, userId: solution.authorId })), + ]) + ), dispatch(fetchSubmissionEvaluationsForSolution(solutionId)), dispatch(fetchAssignmentIfNeeded(assignmentId)), dispatch(fetchAssignmentSolutionFilesIfNeeded(solutionId)), @@ -82,6 +93,8 @@ class Solution extends Component { refreshSolutionEvaluations, scoreConfigSelector, fetchScoreConfigIfNeeded, + assignmentSolversLoading, + assignmentSolverSelector, intl: { locale }, } = this.props; @@ -163,6 +176,8 @@ class Solution extends Component { files={files} download={download} otherSolutions={userSolutionsSelector(solution.authorId, assignment.id)} + assignmentSolversLoading={assignmentSolversLoading} + assignmentSolverSelector={assignmentSolverSelector} assignment={assignment} evaluations={evaluations} runtimeEnvironments={runtimes} @@ -203,6 +218,8 @@ Solution.propTypes = { runtimeEnvironments: PropTypes.array, fetchStatus: PropTypes.string, scoreConfigSelector: PropTypes.func, + assignmentSolversLoading: PropTypes.bool, + assignmentSolverSelector: PropTypes.func.isRequired, editNote: PropTypes.func.isRequired, deleteEvaluation: PropTypes.func.isRequired, refreshSolutionEvaluations: PropTypes.func.isRequired, @@ -227,6 +244,8 @@ export default connect( runtimeEnvironments: assignmentEnvironmentsSelector(state)(assignmentId), fetchStatus: fetchManyStatus(solutionId)(state), scoreConfigSelector: assignmentSubmissionScoreConfigSelector(state), + assignmentSolversLoading: isAssignmentSolversLoading(state), + assignmentSolverSelector: getAssignmentSolverSelector(state), }), (dispatch, { match: { params } }) => ({ loadAsync: () => Solution.loadAsync(params, dispatch), diff --git a/src/redux/modules/solutions.js b/src/redux/modules/solutions.js index fe5f216ea..057c72310 100644 --- a/src/redux/modules/solutions.js +++ b/src/redux/modules/solutions.js @@ -1,4 +1,5 @@ import { handleActions } from 'redux-actions'; +import { fromJS } from 'immutable'; import { createApiAction } from '../middleware/apiMiddleware'; import factory, { @@ -9,6 +10,8 @@ import factory, { } from '../helpers/resourceManager'; import { actionTypes as submissionActionTypes } from './submission'; import { actionTypes as submissionEvaluationActionTypes } from './submissionEvaluations'; +import { getAssignmentSolversLastUpdate } from '../selectors/solutions'; +import { objectFilter } from '../../helpers/common'; const resourceName = 'solutions'; const needsRefetching = item => @@ -51,6 +54,10 @@ export const additionalActionTypes = { SET_FLAG_FULFILLED: 'recodex/solutions/SET_FLAG_FULFILLED', SET_FLAG_REJECTED: 'recodex/solutions/SET_FLAG_REJECTED', DOWNLOAD_RESULT_ARCHIVE: 'recodex/files/DOWNLOAD_RESULT_ARCHIVE', + LOAD_ASSIGNMENT_SOLVERS: 'recodex/solutions/LOAD_ASSIGNMENT_SOLVERS', + LOAD_ASSIGNMENT_SOLVERS_PENDING: 'recodex/solutions/LOAD_ASSIGNMENT_SOLVERS_PENDING', + LOAD_ASSIGNMENT_SOLVERS_FULFILLED: 'recodex/solutions/LOAD_ASSIGNMENT_SOLVERS_FULFILLED', + LOAD_ASSIGNMENT_SOLVERS_REJECTED: 'recodex/solutions/LOAD_ASSIGNMENT_SOLVERS_REJECTED', }; export const fetchSolution = actions.fetchResource; @@ -130,6 +137,25 @@ export const fetchGroupStudentsSolutions = (groupId, userId) => }, }); +export const fetchAssignmentSolvers = ({ assignmentId = null, groupId = null, userId = null }) => + createApiAction({ + type: additionalActionTypes.LOAD_ASSIGNMENT_SOLVERS, + method: 'GET', + endpoint: '/assignment-solvers', + query: objectFilter({ assignmentId, groupId, userId }), + meta: { assignmentId, groupId, userId }, + }); + +export const fetchAssignmentSolversIfNeeded = + ({ assignmentId = null, groupId = null, userId = null }) => + (dispatch, getState) => { + const lastUpdate = getAssignmentSolversLastUpdate(getState(), assignmentId, groupId, userId); + const threshold = 10 * 60 * 1000; // 10 minutes + if (!lastUpdate || Date.now() - lastUpdate > threshold) { + dispatch(fetchAssignmentSolvers({ assignmentId, groupId, userId })); + } + }; + /** * Reducer */ @@ -226,6 +252,25 @@ const reducer = handleActions( return state; }, + [additionalActionTypes.LOAD_ASSIGNMENT_SOLVERS_PENDING]: state => state.set('assignment-solvers-loading', true), + + [additionalActionTypes.LOAD_ASSIGNMENT_SOLVERS_REJECTED]: state => state.set('assignment-solvers-loading', false), + + [additionalActionTypes.LOAD_ASSIGNMENT_SOLVERS_FULFILLED]: ( + state, + { payload, meta: { assignmentId, groupId, userId } } + ) => { + state = state.set('assignment-solvers-loading', false); + state = state.setIn(['assignment-solvers-fetches', `${assignmentId}|${groupId}|${userId}`], Date.now()); + payload.forEach(assignmentSolver => { + state = state.setIn( + ['assignment-solvers', assignmentSolver.assignmentId, assignmentSolver.solverId], + fromJS(assignmentSolver) + ); + }); + return state; + }, + [submissionEvaluationActionTypes.REMOVE_FULFILLED]: (state, { meta: { solutionId, id: evaluationId } }) => { if (!solutionId || !evaluationId) { return state; diff --git a/src/redux/selectors/solutions.js b/src/redux/selectors/solutions.js index 5c7cd488f..a4dbc8a45 100644 --- a/src/redux/selectors/solutions.js +++ b/src/redux/selectors/solutions.js @@ -34,3 +34,20 @@ export const fetchManyGroupStudentsSolutionsStatus = createSelector( solutions => (groupId, userId) => solutions.getIn(['fetchManyStatus', fetchManyGroupStudentsSolutionsEndpoint(groupId, userId)]) ); + +// solvers + +export const getAssignmentSolversLastUpdate = (state, assignmentId, groupId, userId) => + getSolutionsRaw(state).getIn(['assignment-solvers-fetches', `${assignmentId}|${groupId}|${userId}`]); + +export const isAssignmentSolversLoading = state => getSolutionsRaw(state).get('assignment-solvers-loading', false); + +export const getAssignmentSolver = (state, assignmentId, userId) => + getSolutionsRaw(state).getIn(['assignment-solvers', assignmentId, userId]); + +const getAssignmentSolvers = state => getSolutionsRaw(state).get('assignment-solvers'); + +export const getAssignmentSolverSelector = createSelector( + getAssignmentSolvers, + assignmentSolvers => (assignmentId, userId) => assignmentSolvers && assignmentSolvers.getIn([assignmentId, userId]) +);