From 42b946ab11f9b24250b736971d9135fe18ddb809 Mon Sep 17 00:00:00 2001 From: Martin Krulis Date: Tue, 25 Oct 2022 01:37:41 +0200 Subject: [PATCH] Adding notification of pending reviews and "close all" buttons to assignment stats and group user solutions pages. --- src/locales/cs.json | 3 + src/locales/en.json | 3 + src/pages/AssignmentStats/AssignmentStats.js | 57 ++++++++++++++- .../GroupUserSolutions/GroupUserSolutions.js | 70 ++++++++++++++++++- 4 files changed, 129 insertions(+), 4 deletions(-) diff --git a/src/locales/cs.json b/src/locales/cs.json index 991cf3578..817ed4f07 100644 --- a/src/locales/cs.json +++ b/src/locales/cs.json @@ -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)", @@ -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", @@ -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é", diff --git a/src/locales/en.json b/src/locales/en.json index 02a2efb80..39cccb2fd 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -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)", @@ -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", @@ -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", diff --git a/src/pages/AssignmentStats/AssignmentStats.js b/src/pages/AssignmentStats/AssignmentStats.js index deb53265a..0eb6e39cd 100644 --- a/src/pages/AssignmentStats/AssignmentStats.js +++ b/src/pages/AssignmentStats/AssignmentStats.js @@ -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'; @@ -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'; @@ -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 { @@ -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] }, () => { @@ -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 { @@ -309,6 +332,8 @@ class AssignmentStats extends Component { links, } = this.props; + const pendingReviews = getPendingReviewSolutions(assignmentSolutions); + return ( + {pendingReviews && pendingReviews.length > 0 && ( + + + + + + + + + + + )} +
@@ -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, }; @@ -552,6 +604,7 @@ export default withLinks( ev.preventDefault(); dispatch(downloadBestSolutionsArchive(assignmentId, name)); }, + closeReview: id => dispatch(setSolutionReviewState(id, true)), }) )(injectIntl(AssignmentStats)) ); diff --git a/src/pages/GroupUserSolutions/GroupUserSolutions.js b/src/pages/GroupUserSolutions/GroupUserSolutions.js index b1deef338..e5ec2fb6f 100644 --- a/src/pages/GroupUserSolutions/GroupUserSolutions.js +++ b/src/pages/GroupUserSolutions/GroupUserSolutions.js @@ -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'; @@ -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, @@ -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 { @@ -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] }, () => { @@ -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 { @@ -320,6 +356,8 @@ class GroupUserSolutions extends Component { links, } = this.props; + const pendingReviews = getPendingReviewSolutions(assignments, getAssignmentSolutions); + return ( + {pendingReviews && pendingReviews.length > 0 && ( + + + + + + + + + + + )} +
({ loadAsync: () => GroupUserSolutions.loadAsync({ groupId, userId }, dispatch), + closeReview: id => dispatch(setSolutionReviewState(id, true)), }) )(injectIntl(GroupUserSolutions)) );