Skip to content

Commit

Permalink
Additional quick point-overrides implemented as solution actions.
Browse files Browse the repository at this point in the history
  • Loading branch information
krulis-martin committed Jun 20, 2023
1 parent 8b3a184 commit f7b9b76
Show file tree
Hide file tree
Showing 12 changed files with 181 additions and 64 deletions.
35 changes: 22 additions & 13 deletions src/components/Assignments/SolutionsTable/SolutionTableRowIcons.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { FormattedMessage } from 'react-intl';

import SolutionReviewIcon from '../../Solutions/SolutionReviewIcon';
import AssignmentStatusIcon, { getStatusDesc } from '../Assignment/AssignmentStatusIcon';
import CommentsIcon from './CommentsIcon';
import { PlagiarismIcon } from '../../icons';

import withLinks from '../../../helpers/withLinks';

const SolutionTableRowIcons = ({
id,
assignmentId,
accepted,
review = null,
isBestSolution,
Expand All @@ -18,6 +22,7 @@ const SolutionTableRowIcons = ({
commentsStats = null,
isReviewer = false,
plagiarism = false,
links: { SOLUTION_PLAGIARISMS_URI_FACTORY },
}) => (
<>
<AssignmentStatusIcon
Expand All @@ -30,18 +35,20 @@ const SolutionTableRowIcons = ({
{review && <SolutionReviewIcon id={`review-${id}`} review={review} isReviewer={isReviewer} gapLeft />}

{plagiarism && isReviewer && (
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={id}>
<FormattedMessage
id="app.solutionsTable.icons.suspectedPlagiarism"
defaultMessage="Suspected plagiarism (similarities with other solutions were found)"
/>
</Tooltip>
}>
<PlagiarismIcon className="text-danger fa-beat" gapLeft />
</OverlayTrigger>
<Link to={SOLUTION_PLAGIARISMS_URI_FACTORY(assignmentId, id)}>
<OverlayTrigger
placement="right"
overlay={
<Tooltip id={id}>
<FormattedMessage
id="app.solutionsTable.icons.suspectedPlagiarism"
defaultMessage="Suspected plagiarism (similarities with other solutions were found)"
/>
</Tooltip>
}>
<PlagiarismIcon className="text-danger fa-beat" gapLeft />
</OverlayTrigger>
</Link>
)}

<CommentsIcon id={id} commentsStats={commentsStats} gapLeft />
Expand All @@ -50,6 +57,7 @@ const SolutionTableRowIcons = ({

SolutionTableRowIcons.propTypes = {
id: PropTypes.string.isRequired,
assignmentId: PropTypes.string.isRequired,
commentsStats: PropTypes.object,
accepted: PropTypes.bool.isRequired,
review: PropTypes.shape({
Expand All @@ -67,6 +75,7 @@ SolutionTableRowIcons.propTypes = {
}),
isReviewer: PropTypes.bool,
plagiarism: PropTypes.bool,
links: PropTypes.object,
};

export default SolutionTableRowIcons;
export default withLinks(SolutionTableRowIcons);
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const SolutionsTableRow = ({
})}>
<SolutionTableRowIcons
id={id}
assignmentId={assignmentId}
accepted={accepted}
review={review}
isBestSolution={isBestSolution}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ class PendingReviewsList extends Component {
) : (
<Icon icon="boxes-packing" gapRight />
)}
<FormattedMessage id="app.reviewSolutionButtons.close" defaultMessage="Close Review" />
<FormattedMessage
id="app.solution.actions.review.close"
defaultMessage="Close Review"
/>
</Button>
)}
</>
Expand Down
87 changes: 76 additions & 11 deletions src/components/Solutions/SolutionActions/SolutionActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { FormattedMessage } from 'react-intl';
import { withRouter } from 'react-router';

import withLinks from '../../../helpers/withLinks';
import { safeGet } from '../../../helpers/common';

import ActionButton from './ActionButton';
import ActionDropdown from './ActionDropdown';
Expand All @@ -14,42 +15,66 @@ import ActionDropdown from './ActionDropdown';
*/
const actionsTemplates = {
accept: {
short: <FormattedMessage id="app.acceptSolution.notAcceptedShort" defaultMessage="Accept" />,
label: <FormattedMessage id="app.acceptSolution.notAccepted" defaultMessage="Accept as Final" />,
short: <FormattedMessage id="app.solution.actions.accept" defaultMessage="Accept" />,
label: <FormattedMessage id="app.solution.actions.acceptLong" defaultMessage="Accept as Final" />,
icon: ['far', 'check-circle'],
pending: 'acceptPending',
},
unaccept: {
short: <FormattedMessage id="app.acceptSolution.acceptedShort" defaultMessage="Revoke" />,
label: <FormattedMessage id="app.acceptSolution.accepted" defaultMessage="Revoke as Final" />,
short: <FormattedMessage id="app.solution.actions.revokeAccept" defaultMessage="Revoke" />,
label: <FormattedMessage id="app.solution.actions.revokeAcceptLong" defaultMessage="Revoke as Final" />,
icon: ['far', 'circle-xmark'],
variant: 'warning',
pending: 'acceptPending',
},

// point actions
zeroPoints: {
label: <FormattedMessage id="app.solution.actions.points.zero" defaultMessage="Zero Points" />,
icon: 'battery-empty',
variant: 'danger',
pending: 'pointsPending',
},
fullPoints: {
label: <FormattedMessage id="app.solution.actions.points.full" defaultMessage="Full Points" />,
icon: 'battery-full',
pending: 'pointsPending',
},
clearPoints: {
label: <FormattedMessage id="app.solution.actions.points.clearOverride" defaultMessage="Clear Points Override" />,
icon: 'rotate-left',
variant: 'warning',
pending: 'pointsPending',
},

// review actions
open: {
label: <FormattedMessage id="app.reviewSolutionButtons.open" defaultMessage="Start Review" />,
label: <FormattedMessage id="app.solution.actions.review.open" defaultMessage="Start Review" />,
icon: 'microscope',
variant: 'info',
pending: 'updatePending',
},
reopen: {
label: <FormattedMessage id="app.reviewSolutionButtons.reopen" defaultMessage="Reopen Review" />,
label: <FormattedMessage id="app.solution.actions.review.reopen" defaultMessage="Reopen Review" />,
icon: 'person-digging',
variant: 'warning',
pending: 'updatePending',
},
openClose: {
label: <FormattedMessage id="app.reviewSolutionButtons.markReviewed" defaultMessage="Mark as Reviewed" />,
label: <FormattedMessage id="app.solution.actions.review.markReviewed" defaultMessage="Mark as Reviewed" />,
icon: 'file-circle-check',
pending: 'updatePending',
},
close: {
label: <FormattedMessage id="app.reviewSolutionButtons.close" defaultMessage="Close Review" />,
label: <FormattedMessage id="app.solution.actions.review.close" defaultMessage="Close Review" />,
icon: 'boxes-packing',
pending: 'updatePending',
},
delete: {
label: <FormattedMessage id="app.reviewSolutionButtons.delete" defaultMessage="Erase Review" />,
label: <FormattedMessage id="app.solution.actions.review.delete" defaultMessage="Erase Review" />,
icon: 'trash',
variant: 'danger',
pending: 'updatePending',
confirm: (
<FormattedMessage
id="app.reviewSolutionButtons.deleteConfirm"
Expand All @@ -59,11 +84,24 @@ const actionsTemplates = {
},
};

const knownActions = ['accept', 'unaccept', 'open', 'reopen', 'openClose', 'close', 'delete'];
const knownActions = [
'accept',
'unaccept',
'zeroPoints',
'fullPoints',
'clearPoints',
'open',
'reopen',
'openClose',
'close',
'delete',
];

const SolutionActions = ({
id,
solution,
assignment,
pointsPending = false,
acceptPending = false,
showAllButtons = false,
updatePending = false,
Expand All @@ -73,6 +111,7 @@ const SolutionActions = ({
setAccepted = null,
setReviewState = null,
deleteReview = null,
setPoints = null,
history: { push },
location: { pathname },
links: { SOLUTION_SOURCE_CODES_URI_FACTORY },
Expand All @@ -87,13 +126,30 @@ const SolutionActions = ({
if (!permissionHints.review) {
setReviewState = deleteReview = null;
}
if (!permissionHints.setBonusPoints || !showAllButtons) {
setPoints = null;
}

const reviewPageUri = SOLUTION_SOURCE_CODES_URI_FACTORY(assignmentId, id);
const isOnReviewPage = pathname === SOLUTION_SOURCE_CODES_URI_FACTORY(assignmentId, id);

const bonusPoints = solution.bonusPoints;
const points = solution.overriddenPoints !== null ? solution.overriddenPoints : solution.actualPoints;
const evalPoints = safeGet(solution, ['lastSubmission', 'evaluation', 'points']);
const maxPoints = assignment.maxPointsBeforeFirstDeadline;

const actionHandlers = {
accept: !accepted && setAccepted && (() => setAccepted(true)),
unaccept: accepted && setAccepted && (() => setAccepted(false)),
zeroPoints:
setPoints && points !== 0 && evalPoints !== 0 && (() => setPoints({ bonusPoints, overriddenPoints: 0 })),
fullPoints:
setPoints &&
points < maxPoints &&
evalPoints !== maxPoints &&
(() => setPoints({ bonusPoints, overriddenPoints: maxPoints })),
clearPoints:
setPoints && solution.overriddenPoints !== null && (() => setPoints({ bonusPoints, overriddenPoints: null })),
open: setReviewState && (!review || !review.startedAt) && (() => setReviewState(false)),
reopen: setReviewState && review && review.closedAt && (() => setReviewState(false)),
openClose: setReviewState && (!review || !review.startedAt) && showAllButtons && (() => setReviewState(true)),
Expand All @@ -106,12 +162,13 @@ const SolutionActions = ({
actionHandlers.reopen = actionHandlers.reopen && (() => actionHandlers.reopen().then(() => push(reviewPageUri)));
}

const pendingIndicators = { acceptPending, updatePending, pointsPending };
const actions = knownActions
.filter(a => actionHandlers[a])
.map(a => ({
...actionsTemplates[a],
handler: actionHandlers[a],
pending: actionsTemplates[a].pending === 'acceptPending' ? acceptPending : updatePending,
pending: pendingIndicators[actionsTemplates[a].pending] || false,
}));

return dropdown ? (
Expand Down Expand Up @@ -140,14 +197,21 @@ SolutionActions.propTypes = {
solution: PropTypes.shape({
accepted: PropTypes.bool,
assignmentId: PropTypes.string.isRequired,
bonusPoints: PropTypes.number,
actualPoints: PropTypes.number,
overriddenPoints: PropTypes.number,
review: PropTypes.shape({
startedAt: PropTypes.number,
closedAt: PropTypes.number,
issues: PropTypes.number,
}),
permissionHints: PropTypes.object,
}),
assignment: PropTypes.shape({
maxPointsBeforeFirstDeadline: PropTypes.number,
}),
showAllButtons: PropTypes.bool,
pointsPending: PropTypes.bool,
acceptPending: PropTypes.bool,
updatePending: PropTypes.bool,
captionAsTooltip: PropTypes.bool,
Expand All @@ -156,6 +220,7 @@ SolutionActions.propTypes = {
setAccepted: PropTypes.func,
setReviewState: PropTypes.func,
deleteReview: PropTypes.func,
setPoints: PropTypes.func,
history: PropTypes.shape({
push: PropTypes.func.isRequired,
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ const InterpolationDialog = ({
}}
disabled={!interval}>
<SendIcon gapRight />
<FormattedMessage id="" defaultMessage="Update" />
<FormattedMessage id="generic.update" defaultMessage="Update" />
</Button>
<Button variant="secondary" onClick={() => setOpen(false)}>
<CloseIcon gapRight />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,39 @@ import SolutionActions from '../../components/Solutions/SolutionActions';
import ResourceRenderer from '../../components/helpers/ResourceRenderer';

import { setSolutionReviewState, deleteSolutionReview } from '../../redux/modules/solutionReviews';
import { getSolution, isSetFlagPending } from '../../redux/selectors/solutions';
import { setPoints, setSolutionFlag } from '../../redux/modules/solutions';
import { getAssignment } from '../../redux/selectors/assignments';
import { getSolution, isPointsUpdatePending, isSetFlagPending } from '../../redux/selectors/solutions';
import { isSolutionReviewUpdatePending } from '../../redux/selectors/solutionReviews';
import { setSolutionFlag } from '../../redux/modules/solutions';

const SolutionActionsContainer = ({
id,
solution,
assignment,
pointsPending,
updatePending,
setReviewState,
deleteReview,
acceptPending,
setAccepted,
setPoints,
...props
}) => {
return (
<ResourceRenderer resource={solution}>
{solution => (
<ResourceRenderer resource={[solution, assignment]}>
{(solution, assignment) => (
<SolutionActions
{...props}
id={id}
solution={solution}
assignment={assignment}
pointsPending={pointsPending}
acceptPending={acceptPending}
updatePending={updatePending}
setAccepted={setAccepted}
setReviewState={setReviewState}
deleteReview={deleteReview}
setPoints={setPoints}
/>
)}
</ResourceRenderer>
Expand All @@ -42,23 +49,33 @@ const SolutionActionsContainer = ({
SolutionActionsContainer.propTypes = {
id: PropTypes.string.isRequired,
solution: ImmutablePropTypes.map,
assignment: ImmutablePropTypes.map,
pointsPending: PropTypes.bool.isRequired,
acceptPending: PropTypes.bool.isRequired,
updatePending: PropTypes.bool,
setAccepted: PropTypes.func.isRequired,
setReviewState: PropTypes.func.isRequired,
deleteReview: PropTypes.func.isRequired,
setPoints: PropTypes.func.isRequired,
};

const mapStateToProps = (state, { id }) => ({
solution: getSolution(state, id),
acceptPending: isSetFlagPending(state, id, 'accepted'),
updatePending: isSolutionReviewUpdatePending(state, id),
});
const mapStateToProps = (state, { id }) => {
const solution = getSolution(state, id);
const assignmentId = solution && solution.getIn(['data', 'assignmentId']);
return {
solution,
assignment: assignmentId && getAssignment(state)(assignmentId),
pointsPending: isPointsUpdatePending(state, id),
acceptPending: isSetFlagPending(state, id, 'accepted'),
updatePending: isSolutionReviewUpdatePending(state, id),
};
};

const mapDispatchToProps = (dispatch, { id }) => ({
setAccepted: accepted => dispatch(setSolutionFlag(id, 'accepted', accepted)),
setReviewState: closed => dispatch(setSolutionReviewState(id, closed)),
deleteReview: () => dispatch(deleteSolutionReview(id)),
setPoints: ({ overriddenPoints, bonusPoints }) => dispatch(setPoints(id, overriddenPoints, bonusPoints)),
});

export default connect(mapStateToProps, mapDispatchToProps)(SolutionActionsContainer);

0 comments on commit f7b9b76

Please sign in to comment.