Skip to content

Commit ad88159

Browse files
committed
Updating permission checks on group and assignment pages so that locked users are handled correctly.
1 parent 3b4b80b commit ad88159

File tree

12 files changed

+152
-81
lines changed

12 files changed

+152
-81
lines changed

src/components/Assignments/Assignment/AssignmentsTable/AssignmentsTable.js

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,7 @@ class AssignmentsTable extends Component {
177177
isAdmin = false,
178178
showNames = true,
179179
showGroups = false,
180+
noDiscussion = false,
180181
groupsAccessor = null,
181182
onlyCurrent = false,
182183
syncAssignment = null,
@@ -295,7 +296,7 @@ class AssignmentsTable extends Component {
295296
showGroups={showGroups}
296297
showSecondDeadline={showSecondDeadline}
297298
groupsAccessor={groupsAccessor}
298-
discussionOpen={() => this.openDialog(assignment)}
299+
discussionOpen={noDiscussion ? null : () => this.openDialog(assignment)}
299300
setSelected={multiActions ? this.selectAssignmentClickHandler(assignmentsPreprocessedAll) : null}
300301
selected={Boolean(this.state.selectedAssignments[assignment.id])}
301302
doubleClickPush={openOnDoubleclick ? navigate : null}
@@ -438,28 +439,30 @@ class AssignmentsTable extends Component {
438439
)}
439440
</UserUIDataContext.Consumer>
440441

441-
<Modal show={this.state.dialogAssignment !== null} backdrop="static" onHide={this.closeDialog} size="xl">
442-
{this.state.dialogAssignment && (
443-
<CommentThreadContainer
444-
threadId={this.state.dialogAssignment.id}
445-
title={
446-
<>
447-
<FormattedMessage id="app.assignments.discussionModalTitle" defaultMessage="Public Discussion" />:{' '}
448-
<LocalizedExerciseName
449-
entity={{ name: '??', localizedTexts: this.state.dialogAssignment.localizedTexts }}
442+
{!noDiscussion && (
443+
<Modal show={this.state.dialogAssignment !== null} backdrop="static" onHide={this.closeDialog} size="xl">
444+
{this.state.dialogAssignment && (
445+
<CommentThreadContainer
446+
threadId={this.state.dialogAssignment.id}
447+
title={
448+
<>
449+
<FormattedMessage id="app.assignments.discussionModalTitle" defaultMessage="Public Discussion" />:{' '}
450+
<LocalizedExerciseName
451+
entity={{ name: '??', localizedTexts: this.state.dialogAssignment.localizedTexts }}
452+
/>
453+
</>
454+
}
455+
additionalPublicSwitchNote={
456+
<FormattedMessage
457+
id="app.assignments.discussionModal.additionalSwitchNote"
458+
defaultMessage="(supervisors and students of this group)"
450459
/>
451-
</>
452-
}
453-
additionalPublicSwitchNote={
454-
<FormattedMessage
455-
id="app.assignments.discussionModal.additionalSwitchNote"
456-
defaultMessage="(supervisors and students of this group)"
457-
/>
458-
}
459-
displayAs="modal"
460-
/>
461-
)}
462-
</Modal>
460+
}
461+
displayAs="modal"
462+
/>
463+
)}
464+
</Modal>
465+
)}
463466
</>
464467
);
465468
}
@@ -474,6 +477,7 @@ AssignmentsTable.propTypes = {
474477
isAdmin: PropTypes.bool,
475478
showNames: PropTypes.bool,
476479
showGroups: PropTypes.bool,
480+
noDiscussion: PropTypes.bool,
477481
groupsAccessor: PropTypes.func,
478482
onlyCurrent: PropTypes.bool,
479483
syncAssignment: PropTypes.func,

src/components/Groups/GroupExamPending/GroupExamPending.js

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage } from 'react-intl';
4+
import { Link } from 'react-router-dom';
45

56
import ExamLockButtonContainer from '../../../containers/ExamLockButtonContainer';
67
import Callout from '../../widgets/Callout';
7-
import { InfoIcon, GroupExamsIcon } from '../../icons';
8+
import { InfoIcon, GroupExamsIcon, LinkIcon } from '../../icons';
89
import DateTime from '../../widgets/DateTime';
910
import Explanation from '../../widgets/Explanation';
1011

1112
import { isStudentRole } from '../../helpers/usersRoles';
13+
import withLinks from '../../../helpers/withLinks';
1214

1315
const REFRESH_INTERVAL = 1; // [s]
1416

@@ -53,9 +55,24 @@ class GroupExamPending extends Component {
5355
currentUser: {
5456
privateData: { groupLock, isGroupLockStrict, ipLock, role },
5557
},
58+
links: { GROUP_ASSIGNMENTS_URI_FACTORY },
5659
} = this.props;
5760
const isStudent = isStudentRole(role);
5861

62+
if (isStudent && groupLock && groupLock !== id) {
63+
return (
64+
<Callout variant="warning" icon={<GroupExamsIcon />}>
65+
<FormattedMessage
66+
id="app.groupExams.lockedElsewhere"
67+
defaultMessage="You are already locked for an exam in a different group (you can see this group in a read-only mode now)."
68+
/>
69+
<Link to={GROUP_ASSIGNMENTS_URI_FACTORY(groupLock)}>
70+
<LinkIcon gapLeft className="text-primary" />
71+
</Link>
72+
</Callout>
73+
);
74+
}
75+
5976
return (
6077
this.state.isExam && (
6178
<Callout variant={isStudent && !groupLock ? 'danger' : 'warning'} icon={<GroupExamsIcon />}>
@@ -254,6 +271,7 @@ GroupExamPending.propTypes = {
254271
role: PropTypes.string,
255272
}).isRequired,
256273
}),
274+
links: PropTypes.object,
257275
};
258276

259-
export default GroupExamPending;
277+
export default withLinks(GroupExamPending);

src/components/Groups/GroupsTree/GroupsTree.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const defaultButtonsCreator = (
3636
</OverlayTrigger>
3737
)}
3838

39-
{hasPermissions(group, 'viewPublicDetail') && (
39+
{hasPermissions(group, 'viewDetail') && (
4040
<OverlayTrigger
4141
placement="bottom"
4242
overlay={
@@ -52,7 +52,7 @@ const defaultButtonsCreator = (
5252
</OverlayTrigger>
5353
)}
5454

55-
{hasPermissions(group, 'viewDetail') && (
55+
{hasPermissions(group, 'viewAssignments') && (
5656
<OverlayTrigger
5757
placement="bottom"
5858
overlay={

src/components/Solutions/SolutionDetail/SolutionDetail.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import FailureReport from '../../SubmissionFailures/FailureReport';
2222
import Button from '../../widgets/TheButton';
2323
import Callout from '../../widgets/Callout';
2424

25+
import { isStudentLocked } from '../../helpers/exams';
2526
import { safeGet, EMPTY_OBJ } from '../../../helpers/common';
2627

2728
class SolutionDetail extends Component {
@@ -85,6 +86,7 @@ class SolutionDetail extends Component {
8586
canResubmit = false,
8687
assignmentSolversLoading,
8788
assignmentSolverSelector,
89+
currentUser,
8890
} = this.props;
8991

9092
const { openFileId, openFileName, openZipEntry, scoreDialogOpened } = this.state;
@@ -201,15 +203,17 @@ class SolutionDetail extends Component {
201203
/>
202204
)}
203205

204-
<CommentThreadContainer
205-
threadId={id}
206-
additionalPublicSwitchNote={
207-
<FormattedMessage
208-
id="app.solutionDetail.comments.additionalSwitchNote"
209-
defaultMessage="(author of the solution and supervisors of this group)"
210-
/>
211-
}
212-
/>
206+
{!isStudentLocked(currentUser) && (
207+
<CommentThreadContainer
208+
threadId={id}
209+
additionalPublicSwitchNote={
210+
<FormattedMessage
211+
id="app.solutionDetail.comments.additionalSwitchNote"
212+
defaultMessage="(author of the solution and supervisors of this group)"
213+
/>
214+
}
215+
/>
216+
)}
213217
</Col>
214218

215219
{evaluations && (
@@ -311,6 +315,7 @@ SolutionDetail.propTypes = {
311315
assignmentSolverSelector: PropTypes.func.isRequired,
312316
assignment: PropTypes.object.isRequired,
313317
evaluations: PropTypes.object.isRequired,
318+
currentUser: PropTypes.object.isRequired,
314319
runtimeEnvironments: PropTypes.array,
315320
editNote: PropTypes.func,
316321
deleteEvaluation: PropTypes.func,

src/components/helpers/exams.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Check whether given student (user) is locked in a group
3+
* @param {Object} student being tested
4+
* @param {string|null} groupId if null, any group lock is considered
5+
* @param {number|null} now when the test is performed (unix ts), null = now
6+
* @returns {Boolean} true if the student is locked in the group
7+
*/
8+
export const isStudentLocked = (student, groupId = null, now = null) => {
9+
if (!student?.privateData?.groupLock) {
10+
return false; // no lock
11+
}
12+
13+
if (!now) {
14+
now = Date.now() / 1000;
15+
}
16+
17+
if (student?.privateData?.groupLockExpiration && student.privateData.groupLockExpiration < now) {
18+
return false; // lock expired
19+
}
20+
21+
// if groupId is set, only lock within that group counts
22+
return groupId ? student.privateData.groupLock === groupId : true;
23+
};

src/containers/AssignmentsTableContainer/AssignmentsTableContainer.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ class AssignmentsTableContainer extends Component {
4040
stats,
4141
onlyCurrent = false,
4242
hideEmpty = false,
43+
noDiscussion = false,
4344
} = this.props;
4445
return hideEmpty && assignments.size === 0 ? null : (
4546
<ResourceRenderer
@@ -60,6 +61,7 @@ class AssignmentsTableContainer extends Component {
6061
stats={userStats}
6162
statuses={userStats && userStats.assignments}
6263
onlyCurrent={onlyCurrent}
64+
noDiscussion={noDiscussion}
6365
/>
6466
);
6567
}}
@@ -73,6 +75,7 @@ AssignmentsTableContainer.propTypes = {
7375
userId: PropTypes.string,
7476
onlyCurrent: PropTypes.bool,
7577
hideEmpty: PropTypes.bool,
78+
noDiscussion: PropTypes.bool,
7679
assignments: ImmutablePropTypes.list,
7780
assignmentEnvironmentsSelector: PropTypes.func,
7881
stats: ImmutablePropTypes.map,

src/pages/Assignment/Assignment.js

Lines changed: 24 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import {
3737
getAssignmentSolverSelector,
3838
} from '../../redux/selectors/solutions';
3939
import { loggedUserIsStudentOfSelector } from '../../redux/selectors/usersGroups';
40+
import { loggedInUserSelector } from '../../redux/selectors/users';
4041

4142
import Page from '../../components/layout/Page';
4243
import { AssignmentNavigation } from '../../components/layout/Navigation';
@@ -53,6 +54,7 @@ import CommentThreadContainer from '../../containers/CommentThreadContainer';
5354

5455
import LoadingSolutionsTable from '../../components/Assignments/SolutionsTable/LoadingSolutionsTable';
5556
import FailedLoadingSolutionsTable from '../../components/Assignments/SolutionsTable/FailedLoadingSolutionsTable';
57+
import { isStudentLocked } from '../../components/helpers/exams';
5658
import { hasPermissions } from '../../helpers/common';
5759

5860
const getReason = ({ lockedReason }, locale) =>
@@ -96,6 +98,7 @@ class Assignment extends Component {
9698
submitting,
9799
userId,
98100
loggedInUserId,
101+
currentUser,
99102
init,
100103
isStudentOf,
101104
canSubmit,
@@ -110,10 +113,10 @@ class Assignment extends Component {
110113

111114
return (
112115
<Page
113-
resource={assignment}
116+
resource={[assignment, currentUser]}
114117
icon={<AssignmentIcon />}
115118
title={<FormattedMessage id="app.assignment.title" defaultMessage="Assignment Detail" />}>
116-
{assignment => (
119+
{(assignment, currentUser) => (
117120
<div>
118121
<AssignmentNavigation
119122
assignmentId={assignment.id}
@@ -261,21 +264,23 @@ class Assignment extends Component {
261264
</Box>
262265
)}
263266

264-
<CommentThreadContainer
265-
threadId={assignment.id}
266-
title={
267-
<FormattedMessage
268-
id="app.assignments.discussionModalTitle"
269-
defaultMessage="Public Discussion"
270-
/>
271-
}
272-
additionalPublicSwitchNote={
273-
<FormattedMessage
274-
id="app.assignments.discussionModal.additionalSwitchNote"
275-
defaultMessage="(supervisors and students of this group)"
276-
/>
277-
}
278-
/>
267+
{!isStudentLocked(currentUser) && (
268+
<CommentThreadContainer
269+
threadId={assignment.id}
270+
title={
271+
<FormattedMessage
272+
id="app.assignments.discussionModalTitle"
273+
defaultMessage="Public Discussion"
274+
/>
275+
}
276+
additionalPublicSwitchNote={
277+
<FormattedMessage
278+
id="app.assignments.discussionModal.additionalSwitchNote"
279+
defaultMessage="(supervisors and students of this group)"
280+
/>
281+
}
282+
/>
283+
)}
279284
</Col>
280285
</Row>
281286
)}
@@ -290,6 +295,7 @@ class Assignment extends Component {
290295
Assignment.propTypes = {
291296
userId: PropTypes.string,
292297
loggedInUserId: PropTypes.string,
298+
currentUser: ImmutablePropTypes.map,
293299
params: PropTypes.shape({
294300
assignmentId: PropTypes.string.isRequired,
295301
userId: PropTypes.string,
@@ -321,6 +327,7 @@ export default injectIntl(
321327
runtimeEnvironments: assignmentEnvironmentsSelector(state)(assignmentId),
322328
userId,
323329
loggedInUserId,
330+
currentUser: loggedInUserSelector(state),
324331
isStudentOf: loggedUserIsStudentOfSelector(state),
325332
canSubmit: canSubmitSolution(assignmentId)(state),
326333
solutions: getUserSolutionsSortedData(state)(userId || loggedInUserId, assignmentId),

src/pages/Dashboard/Dashboard.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,13 @@ class Dashboard extends Component {
183183
}
184184
unlimitedHeight>
185185
<>
186-
<AssignmentsTableContainer userId={user.id} groupId={groupId} onlyCurrent hideEmpty />
186+
<AssignmentsTableContainer
187+
userId={user.id}
188+
groupId={groupId}
189+
onlyCurrent
190+
hideEmpty
191+
noDiscussion
192+
/>
187193
<ShadowAssignmentsTableContainer userId={user.id} groupId={groupId} hideEmpty />
188194
</>
189195
</Box>

src/pages/GroupAssignments/GroupAssignments.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import ShadowAssignmentsTable from '../../components/Assignments/ShadowAssignmen
1717
import GroupArchivedWarning from '../../components/Groups/GroupArchivedWarning/GroupArchivedWarning';
1818
import GroupExamPending from '../../components/Groups/GroupExamPending';
1919
import ResourceRenderer from '../../components/helpers/ResourceRenderer';
20+
import { isStudentLocked } from '../../components/helpers/exams';
2021
import LeaveJoinGroupButtonContainer from '../../containers/LeaveJoinGroupButtonContainer';
2122
import ExercisesListContainer from '../../containers/ExercisesListContainer';
2223

@@ -220,6 +221,7 @@ class GroupAssignments extends Component {
220221
stats={groupStats.find(item => item.userId === userId)}
221222
userId={isGroupAdmin || isGroupSupervisor ? null : userId}
222223
isAdmin={isGroupAdmin || isGroupSupervisor || isSuperadminRole(effectiveRole)}
224+
noDiscussion={isStudentLocked(currentUser)}
223225
/>
224226
)}
225227
</ResourceRenderer>

0 commit comments

Comments
 (0)