Skip to content

Commit

Permalink
Adding notification of pending reviews and "close all" buttons to ass…
Browse files Browse the repository at this point in the history
…ignment stats and group user solutions pages.
  • Loading branch information
krulis-martin committed Oct 30, 2022
1 parent 101286e commit 42b946a
Show file tree
Hide file tree
Showing 4 changed files with 129 additions and 4 deletions.
3 changes: 3 additions & 0 deletions src/locales/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"app.assignmentStats.groupByUsersCheckbox": "Seskupit dle uživatelů",
"app.assignmentStats.noSolutions": "Momentálně zde nejsou žádná odevzdaná řešení.",
"app.assignmentStats.onlyBestSolutionsCheckbox": "Pouze nejlepší řešení",
"app.assignmentStats.pendingReviews": "V tuto chvíli {count, plural, one {je otevřena} =2 {jsou otevřeny} =3 {jsou otevřeny} =4 {jsou otevřeny} other {je otevřeno}} {count} {count, plural, one {revize} =2 {revize} =3 {revize} =4 {revize} other {revizí}} ze všech řešení vybrané úlohy. Nezapomeňte, že autoři úloh vidí vaše komentáře zdrojových kódů až poté, co jsou příslušné revize uzavřeny.",
"app.assignmentStats.title": "Všechna řešení zadané úlohy",
"app.assignments.deadline": "Termín odevzdání",
"app.assignments.discussionModal.additionalSwitchNote": "(vedoucí a studenti této skupiny)",
Expand Down Expand Up @@ -1001,6 +1002,7 @@
"app.groupUserSolutions.groupByAssignmentsCheckbox": "Seskupit podle úloh",
"app.groupUserSolutions.noSolutions": "Uživatel zatím neodevzdal žádná řešení.",
"app.groupUserSolutions.onlyBestSolutionsCheckbox": "Pouze nejlepší řešení",
"app.groupUserSolutions.pendingReviews": "V tuto chvíli {count, plural, one {je otevřena} =2 {jsou otevřeny} =3 {jsou otevřeny} =4 {jsou otevřeny} other {je otevřeno}} {count} {count, plural, one {revize} =2 {revize} =3 {revize} =4 {revize} other {revizí}} ze všech řešení vybraného uživatele. Nezapomeňte, že autoři úloh vidí vaše komentáře zdrojových kódů až poté, co jsou příslušné revize uzavřeny.",
"app.groupUserSolutions.title": "Všechna odevzdaná řešení jednoho uživatele",
"app.groupUserSolutions.userSolutions": "Řešení uživatele",
"app.groups.joinGroupButton": "Stát se členem",
Expand Down Expand Up @@ -1442,6 +1444,7 @@
"app.reviewCommentForm.suppressNotification": "Neodesílat oznámení",
"app.reviewCommentForm.suppressNotificationExplanation": "Poté, co byla revize uzavřena, se s každou provedenou změnou posílá oznámení autorovi. Můžete potlačit odeslání oznámení, pokud provádíte pouze drobnou úpravu.",
"app.reviewSolutionButtons.close": "Uzavřít revizi",
"app.reviewSolutionButtons.closePendingReviews": "Uzavřít probíhající revize",
"app.reviewSolutionButtons.delete": "Smazat revizi",
"app.reviewSolutionButtons.deleteConfirm": "Všechny komentáře v kódu budou smazány společně s revizí. Opravdu si přejete smazat?",
"app.reviewSolutionButtons.markReviewed": "Označit za revidované",
Expand Down
3 changes: 3 additions & 0 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"app.assignmentStats.groupByUsersCheckbox": "Group by users",
"app.assignmentStats.noSolutions": "There are currently no submitted solutions.",
"app.assignmentStats.onlyBestSolutionsCheckbox": "Best solutions only",
"app.assignmentStats.pendingReviews": "There {count, plural, one {is} other {are}} {count} pending {count, plural, one {review} other {reviews}} among the solutions of the selected assignment. Remember that the review comments are visible to the author after a review is closed.",
"app.assignmentStats.title": "All Submissions of The Assignment",
"app.assignments.deadline": "Deadline",
"app.assignments.discussionModal.additionalSwitchNote": "(supervisors and students of this group)",
Expand Down Expand Up @@ -1001,6 +1002,7 @@
"app.groupUserSolutions.groupByAssignmentsCheckbox": "Group by assignments",
"app.groupUserSolutions.noSolutions": "The user has not submitted any solutions yet.",
"app.groupUserSolutions.onlyBestSolutionsCheckbox": "Best solutions only",
"app.groupUserSolutions.pendingReviews": "There {count, plural, one {is} other {are}} {count} pending {count, plural, one {review} other {reviews}} among the solutions of the selected user. Remember that the review comments are visible to the author after a review is closed.",
"app.groupUserSolutions.title": "All Submissions of Selected User",
"app.groupUserSolutions.userSolutions": "User Solutions",
"app.groups.joinGroupButton": "Join group",
Expand Down Expand Up @@ -1442,6 +1444,7 @@
"app.reviewCommentForm.suppressNotification": "Suppress e-mail notification",
"app.reviewCommentForm.suppressNotificationExplanation": "When the review is closed, a notification is sent to the author with every change. You may suppress the notification if the change you are performing is not significant.",
"app.reviewSolutionButtons.close": "Close Review",
"app.reviewSolutionButtons.closePendingReviews": "Close pending reviews",
"app.reviewSolutionButtons.delete": "Erase Review",
"app.reviewSolutionButtons.deleteConfirm": "All review comments will be erased as well. Do you wish to proceed?",
"app.reviewSolutionButtons.markReviewed": "Mark as Reviewed",
Expand Down
57 changes: 55 additions & 2 deletions src/pages/AssignmentStats/AssignmentStats.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import CommentThreadContainer from '../../containers/CommentThreadContainer';

import Page from '../../components/layout/Page';
import { AssignmentNavigation } from '../../components/layout/Navigation';
import { ChatIcon, DownloadIcon, DetailIcon, ResultsIcon, UserIcon } from '../../components/icons';
import Icon, { ChatIcon, DownloadIcon, DetailIcon, LoadingIcon, ResultsIcon, UserIcon } from '../../components/icons';
import SolutionTableRowIcons from '../../components/Assignments/SolutionsTable/SolutionTableRowIcons';
import UsersName from '../../components/Users/UsersName';
import Points from '../../components/Assignments/SolutionsTable/Points';
Expand All @@ -32,12 +32,14 @@ import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourc
import { createUserNameComparator } from '../../components/helpers/users';
import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames';
import EnvironmentsListItem from '../../components/helpers/EnvironmentsList/EnvironmentsListItem';
import Callout from '../../components/widgets/Callout';

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, fetchAssignmentSolversIfNeeded } from '../../redux/modules/solutions';
import { setSolutionReviewState } from '../../redux/modules/solutionReviews';
import { usersSelector } from '../../redux/selectors/users';
import { groupSelector } from '../../redux/selectors/groups';
import { studentsIdsOfGroup } from '../../redux/selectors/usersGroups';
Expand Down Expand Up @@ -235,6 +237,13 @@ const prepareTableData = defaultMemoize(
}
);

const getPendingReviewSolutions = defaultMemoize(assignmentSolutions =>
assignmentSolutions
.toArray()
.map(getJsData)
.filter(solution => solution && solution.review && solution.review.startedAt && !solution.review.closedAt)
);

const localStorageStateKey = 'AssignmentStats.state';

class AssignmentStats extends Component {
Expand All @@ -252,7 +261,13 @@ class AssignmentStats extends Component {
dispatch(fetchAssignmentSolutions(assignmentId)),
]);

state = { groupByUsersCheckbox: true, onlyBestSolutionsCheckbox: false, assignmentDialogOpen: false };
state = {
groupByUsersCheckbox: true,
onlyBestSolutionsCheckbox: false,
assignmentDialogOpen: false,
closingReviews: false,
closingReviewsFailed: false,
};

checkboxClickHandler = ev => {
this.setState({ [ev.target.name]: !this.state[ev.target.name] }, () => {
Expand Down Expand Up @@ -290,6 +305,14 @@ class AssignmentStats extends Component {
return `${safeName || assignmentId}.zip`;
};

closeReviews = solutions => {
this.setState({ closingReviews: true, closingReviewsFailed: false });
return Promise.all(solutions.map(({ id }) => this.props.closeReview(id))).then(
() => this.setState({ closingReviews: false }),
() => this.setState({ closingReviews: false, closingReviewsFailed: true })
);
};

// Re-format the data, so they can be rendered by the SortableTable ...
render() {
const {
Expand All @@ -309,6 +332,8 @@ class AssignmentStats extends Component {
links,
} = this.props;

const pendingReviews = getPendingReviewSolutions(assignmentSolutions);

return (
<Page
resource={assignment}
Expand All @@ -325,6 +350,32 @@ class AssignmentStats extends Component {
canViewExercise={true}
/>

{pendingReviews && pendingReviews.length > 0 && (
<Callout variant="warning">
<Row className="align-items-center">
<Col className="pr-3 py-2">
<FormattedMessage
id="app.assignmentStats.pendingReviews"
defaultMessage="There {count, plural, one {is} other {are}} {count} pending {count, plural, one {review} other {reviews}} among the solutions of the selected assignment. Remember that the review comments are visible to the author after a review is closed."
values={{ count: pendingReviews.length }}
/>
</Col>
<Col xl="auto">
<Button
variant={this.state.closingReviewsFailed ? 'danger' : 'success'}
onClick={() => this.closeReviews(pendingReviews)}
disabled={this.state.closingReviews}>
{this.state.closingReviews ? <LoadingIcon gapRight /> : <Icon icon="boxes-packing" gapRight />}
<FormattedMessage
id="app.reviewSolutionButtons.closePendingReviews"
defaultMessage="Close pending reviews"
/>
</Button>
</Col>
</Row>
</Callout>
)}

<Row>
<Col md={12} lg={7}>
<div className="mb-3">
Expand Down Expand Up @@ -506,6 +557,7 @@ AssignmentStats.propTypes = {
fetchManyStatus: PropTypes.string,
assignmentSolversLoading: PropTypes.bool,
assignmentSolverSelector: PropTypes.func.isRequired,
closeReview: PropTypes.func.isRequired,
intl: PropTypes.object,
links: PropTypes.object.isRequired,
};
Expand Down Expand Up @@ -552,6 +604,7 @@ export default withLinks(
ev.preventDefault();
dispatch(downloadBestSolutionsArchive(assignmentId, name));
},
closeReview: id => dispatch(setSolutionReviewState(id, true)),
})
)(injectIntl(AssignmentStats))
);
70 changes: 68 additions & 2 deletions src/pages/GroupUserSolutions/GroupUserSolutions.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import ReviewSolutionContainer from '../../containers/ReviewSolutionContainer';

import Page from '../../components/layout/Page';
import { GroupNavigation } from '../../components/layout/Navigation';
import { AssignmentIcon, DetailIcon, UserIcon } from '../../components/icons';
import Icon, { AssignmentIcon, DetailIcon, LoadingIcon, UserIcon } from '../../components/icons';
import SolutionTableRowIcons from '../../components/Assignments/SolutionsTable/SolutionTableRowIcons';
import Points from '../../components/Assignments/SolutionsTable/Points';
import SolutionsTable from '../../components/Assignments/SolutionsTable';
Expand All @@ -27,12 +27,14 @@ import FetchManyResourceRenderer from '../../components/helpers/FetchManyResourc
import { LocalizedExerciseName } from '../../components/helpers/LocalizedNames';
import EnvironmentsListItem from '../../components/helpers/EnvironmentsList/EnvironmentsListItem';
import GroupArchivedWarning from '../../components/Groups/GroupArchivedWarning/GroupArchivedWarning';
import Callout from '../../components/widgets/Callout';

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, fetchAssignmentSolversIfNeeded } from '../../redux/modules/solutions';
import { setSolutionReviewState } from '../../redux/modules/solutionReviews';
import { groupSelector, groupsAssignmentsSelector, groupDataAccessorSelector } from '../../redux/selectors/groups';
import {
assignmentEnvironmentsSelector,
Expand Down Expand Up @@ -257,6 +259,27 @@ const prepareTableData = defaultMemoize(
}
);

const getPendingReviewSolutions = defaultMemoize((assignments, getAssignmentSolutions) =>
assignments
? assignments
.toArray()
.map(getJsData)
.filter(identity)
.reduce(
(acc, assignment) => [
...acc,
...getAssignmentSolutions(assignment.id)
.toArray()
.map(getJsData)
.filter(
solution => solution && solution.review && solution.review.startedAt && !solution.review.closedAt
),
],
[]
)
: []
);

const localStorageStateKey = 'GroupUserSolutions.state';

class GroupUserSolutions extends Component {
Expand All @@ -277,7 +300,12 @@ class GroupUserSolutions extends Component {
dispatch(fetchRuntimeEnvironments()),
]);

state = { groupByAssignmentsCheckbox: true, onlyBestSolutionsCheckbox: false };
state = {
groupByAssignmentsCheckbox: true,
onlyBestSolutionsCheckbox: false,
closingReviews: false,
closingReviewsFailed: false,
};

checkboxClickHandler = ev => {
this.setState({ [ev.target.name]: !this.state[ev.target.name] }, () => {
Expand All @@ -300,6 +328,14 @@ class GroupUserSolutions extends Component {
}
}

closeReviews = solutions => {
this.setState({ closingReviews: true, closingReviewsFailed: false });
return Promise.all(solutions.map(({ id }) => this.props.closeReview(id))).then(
() => this.setState({ closingReviews: false }),
() => this.setState({ closingReviews: false, closingReviewsFailed: true })
);
};

// Re-format the data, so they can be rendered by the SortableTable ...
render() {
const {
Expand All @@ -320,6 +356,8 @@ class GroupUserSolutions extends Component {
links,
} = this.props;

const pendingReviews = getPendingReviewSolutions(assignments, getAssignmentSolutions);

return (
<Page
resource={group}
Expand All @@ -342,6 +380,32 @@ class GroupUserSolutions extends Component {
linkFactory={links.GROUP_EDIT_URI_FACTORY}
/>

{pendingReviews && pendingReviews.length > 0 && (
<Callout variant="warning">
<Row className="align-items-center">
<Col className="pr-3 py-2">
<FormattedMessage
id="app.groupUserSolutions.pendingReviews"
defaultMessage="There {count, plural, one {is} other {are}} {count} pending {count, plural, one {review} other {reviews}} among the solutions of the selected user. Remember that the review comments are visible to the author after a review is closed."
values={{ count: pendingReviews.length }}
/>
</Col>
<Col xl="auto">
<Button
variant={this.state.closingReviewsFailed ? 'danger' : 'success'}
onClick={() => this.closeReviews(pendingReviews)}
disabled={this.state.closingReviews}>
{this.state.closingReviews ? <LoadingIcon gapRight /> : <Icon icon="boxes-packing" gapRight />}
<FormattedMessage
id="app.reviewSolutionButtons.closePendingReviews"
defaultMessage="Close pending reviews"
/>
</Button>
</Col>
</Row>
</Callout>
)}

<div className="text-right text-nowrap py-2">
<OnOffCheckbox
className="text-left mr-3"
Expand Down Expand Up @@ -483,6 +547,7 @@ GroupUserSolutions.propTypes = {
assignmentSolversLoading: PropTypes.bool,
assignmentSolverSelector: PropTypes.func.isRequired,
loadAsync: PropTypes.func.isRequired,
closeReview: PropTypes.func.isRequired,
intl: PropTypes.object,
links: PropTypes.object.isRequired,
};
Expand Down Expand Up @@ -522,6 +587,7 @@ export default withLinks(
}
) => ({
loadAsync: () => GroupUserSolutions.loadAsync({ groupId, userId }, dispatch),
closeReview: id => dispatch(setSolutionReviewState(id, true)),
})
)(injectIntl(GroupUserSolutions))
);

0 comments on commit 42b946a

Please sign in to comment.