From 4fb72ea00f84335d72ec6aca066af842c01b55a2 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Tue, 30 Apr 2019 16:58:12 +0200 Subject: [PATCH] Adding accept action buttons to assignment solutions overview. --- .../SolutionsTable/SolutionsTableRow.js | 11 +++- .../buttons/AcceptSolution/AcceptSolution.js | 20 ++++-- .../AcceptedSolutionContainer.js | 6 +- src/pages/AssignmentStats/AssignmentStats.js | 8 ++- src/redux/modules/solutions.js | 62 ++++++++++++------- 5 files changed, 72 insertions(+), 35 deletions(-) diff --git a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js index 319bbfd6e..edd604da6 100644 --- a/src/components/Assignments/SolutionsTable/SolutionsTableRow.js +++ b/src/components/Assignments/SolutionsTable/SolutionsTableRow.js @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { FormattedMessage, FormattedNumber } from 'react-intl'; +import { FormattedMessage, FormattedNumber, injectIntl, intlShape } from 'react-intl'; import { Link } from 'react-router'; import { OverlayTrigger, Tooltip } from 'react-bootstrap'; import classnames from 'classnames'; @@ -9,6 +9,8 @@ import AssignmentStatusIcon, { getStatusDesc } from '../Assignment/AssignmentSta import Points from './Points'; import EnvironmentsListItem from '../../helpers/EnvironmentsList/EnvironmentsListItem'; import DeleteSolutionButtonContainer from '../../../containers/DeleteSolutionButtonContainer/DeleteSolutionButtonContainer'; +import AcceptSolutionContainer from '../../../containers/AcceptSolutionContainer'; + import CommentsIcon from './CommentsIcon'; import { SendIcon } from '../../icons'; import DateTime from '../../widgets/DateTime'; @@ -36,6 +38,7 @@ const SolutionsTableRow = ({ noteMaxlen = null, compact = false, links: { SOLUTION_DETAIL_URI_FACTORY }, + intl: { locale }, }) => { const trimmedNote = note && note.trim(); const hasNote = Boolean(trimmedNote); @@ -118,6 +121,9 @@ const SolutionsTableRow = ({ )} + {permissionHints && permissionHints.setAccepted && ( + + )} {permissionHints && permissionHints.delete && } @@ -162,6 +168,7 @@ SolutionsTableRow.propTypes = { noteMaxlen: PropTypes.number, compact: PropTypes.bool.isRequired, links: PropTypes.object, + intl: intlShape, }; -export default withLinks(SolutionsTableRow); +export default withLinks(injectIntl(SolutionsTableRow)); diff --git a/src/components/buttons/AcceptSolution/AcceptSolution.js b/src/components/buttons/AcceptSolution/AcceptSolution.js index df8664b19..ddd81fe92 100644 --- a/src/components/buttons/AcceptSolution/AcceptSolution.js +++ b/src/components/buttons/AcceptSolution/AcceptSolution.js @@ -4,16 +4,24 @@ import { FormattedMessage } from 'react-intl'; import Button from '../../widgets/FlatButton'; import Icon from '../../icons'; -const AcceptSolution = ({ accepted, acceptPending, accept, unaccept }) => +const AcceptSolution = ({ accepted, acceptPending, accept, unaccept, shortLabel = false, bsSize = undefined }) => accepted === true ? ( - ) : ( - ); @@ -22,6 +30,8 @@ AcceptSolution.propTypes = { acceptPending: PropTypes.bool.isRequired, accept: PropTypes.func.isRequired, unaccept: PropTypes.func.isRequired, + shortLabel: PropTypes.bool, + bsSize: PropTypes.string, }; export default AcceptSolution; diff --git a/src/containers/AcceptSolutionContainer/AcceptedSolutionContainer.js b/src/containers/AcceptSolutionContainer/AcceptedSolutionContainer.js index 31f1d3067..12a6289dd 100644 --- a/src/containers/AcceptSolutionContainer/AcceptedSolutionContainer.js +++ b/src/containers/AcceptSolutionContainer/AcceptedSolutionContainer.js @@ -6,8 +6,10 @@ import AcceptSolution from '../../components/buttons/AcceptSolution'; import { acceptSolution, unacceptSolution } from '../../redux/modules/solutions'; import { isAccepted, isAcceptPending } from '../../redux/selectors/solutions'; -const AcceptSolutionContainer = ({ accepted, acceptPending, accept, unaccept }) => { - return ; +const AcceptSolutionContainer = ({ accepted, acceptPending, accept, unaccept, ...props }) => { + return ( + + ); }; AcceptSolutionContainer.propTypes = { diff --git a/src/pages/AssignmentStats/AssignmentStats.js b/src/pages/AssignmentStats/AssignmentStats.js index fcb136a00..5e4297245 100644 --- a/src/pages/AssignmentStats/AssignmentStats.js +++ b/src/pages/AssignmentStats/AssignmentStats.js @@ -3,7 +3,7 @@ import PropTypes from 'prop-types'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { Row, Col } from 'react-bootstrap'; import { connect } from 'react-redux'; -import { injectIntl, FormattedMessage, FormattedNumber } from 'react-intl'; +import { injectIntl, FormattedMessage, FormattedNumber, intlShape } from 'react-intl'; import { LinkContainer } from 'react-router-bootstrap'; import { Link } from 'react-router'; import { defaultMemoize } from 'reselect'; @@ -40,6 +40,7 @@ import DateTime from '../../components/widgets/DateTime'; import Points from '../../components/Assignments/SolutionsTable/Points'; import EnvironmentsListItem from '../../components/helpers/EnvironmentsList/EnvironmentsListItem'; import DeleteSolutionButtonContainer from '../../containers/DeleteSolutionButtonContainer/DeleteSolutionButtonContainer'; +import AcceptSolutionContainer from '../../containers/AcceptSolutionContainer'; import { safeGet, identity } from '../../helpers/common'; import { createUserNameComparator } from '../../components/helpers/users'; @@ -161,6 +162,9 @@ const prepareTableColumnDescriptors = defaultMemoize((loggedUserId, assignmentId )} + {solution.permissionHints && solution.permissionHints.setAccepted && ( + + )} {solution.permissionHints && solution.permissionHints.delete && ( )} @@ -442,7 +446,7 @@ AssignmentStats.propTypes = { loadAsync: PropTypes.func.isRequired, downloadBestSolutionsArchive: PropTypes.func.isRequired, fetchManyStatus: PropTypes.string, - intl: PropTypes.shape({ locale: PropTypes.string.isRequired }).isRequired, + intl: intlShape, links: PropTypes.object.isRequired, }; diff --git a/src/redux/modules/solutions.js b/src/redux/modules/solutions.js index 15aa42ac5..01a686c42 100644 --- a/src/redux/modules/solutions.js +++ b/src/redux/modules/solutions.js @@ -164,18 +164,30 @@ const reducer = handleActions( [additionalActionTypes.ACCEPT_REJECTED]: (state, { meta: { id } }) => state.setIn(['resources', id, 'data', 'accepted-pending'], false), - [additionalActionTypes.ACCEPT_FULFILLED]: (state, { meta: { id } }) => - state.update('resources', resources => - resources.map((item, itemId) => - item.get('data') !== null - ? item.update('data', data => - itemId === id - ? data.set('accepted', true).set('accepted-pending', false) - : data.set('accepted', false).set('accepted-pending', false) - ) - : item - ) - ), + [additionalActionTypes.ACCEPT_FULFILLED]: (state, { meta: { id } }) => { + const assignmentId = state.getIn(['resources', id, 'data', 'exerciseAssignmentId']); + const userId = state.getIn(['resources', id, 'data', 'solution', 'userId']); + return !assignmentId || !userId + ? state + : state + // Accepted solution needs to be updated + .updateIn(['resources', id, 'data'], data => + data + .set('accepted', true) + .set('isBestSolution', true) // accepted also becomes best solution + .set('accepted-pending', false) + ) + // All other solutions from the same assignment by the same author needs to be updated + .update('resources', resources => + resources.map((item, itemId) => { + const aId = item.getIn(['data', 'exerciseAssignmentId']); + const uId = item.getIn(['data', 'solution', 'userId']); + return itemId === id || aId !== assignmentId || uId !== userId + ? item // no modification (either it is accepted solution, or it is solution from another assignment/by another user) + : item.update('data', data => data.set('accepted', false).set('isBestSolution', false)); // no other solution can be accepted nor best + }) + ); + }, [additionalActionTypes.UNACCEPT_PENDING]: (state, { meta: { id } }) => state.setIn(['resources', id, 'data', 'accepted-pending'], true), @@ -183,18 +195,20 @@ const reducer = handleActions( [additionalActionTypes.UNACCEPT_REJECTED]: (state, { meta: { id } }) => state.setIn(['resources', id, 'data', 'accepted-pending'], false), - [additionalActionTypes.UNACCEPT_FULFILLED]: (state, { meta: { id } }) => - state.update('resources', resources => - resources.map((item, itemId) => - item.get('data') !== null - ? item.update('data', data => - itemId === id - ? data.set('accepted', false).set('accepted-pending', false) - : data.set('accepted', true).set('accepted-pending', false) - ) - : item - ) - ), + [additionalActionTypes.UNACCEPT_FULFILLED]: (state, { payload: { assignments }, meta: { id } }) => { + const assignmentId = state.getIn(['resources', id, 'data', 'exerciseAssignmentId']); + const assignmentStats = assignments.find(a => a.id === assignmentId); + const newBestSolutionId = assignmentStats && assignmentStats.bestSolutionId; + state = state.updateIn(['resources', id, 'data'], data => + data + .set('accepted', false) + .set('isBestSolution', false) + .set('accepted-pending', false) + ); + return newBestSolutionId && state.hasIn(['resources', newBestSolutionId, 'data', 'isBestSolution']) + ? state.setIn(['resources', newBestSolutionId, 'data', 'isBestSolution'], true) + : state; + }, [submissionEvaluationActionTypes.REMOVE_FULFILLED]: (state, { meta: { solutionId, id: evaluationId } }) => { if (!solutionId || !evaluationId) {