+ {attemptIndex}. |
+
id === runtimeEnvironmentId)
}
otherSolutions={otherSolutions}
+ assignmentSolversLoading={assignmentSolversLoading}
+ assignmentSolverSelector={assignmentSolverSelector}
/>
@@ -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 {
:
-
+ {!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])
+);
| |