Skip to content

Commit 6a635c8

Browse files
committed
Adding collective update functions for exercise assignments (e.g., syncing multiple assignments with the exercise at once).
1 parent 1d36bc4 commit 6a635c8

File tree

18 files changed

+425
-81
lines changed

18 files changed

+425
-81
lines changed

src/components/Assignments/Assignment/AssignmentSync/AssignmentSync.js

Lines changed: 2 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,57 +6,7 @@ import { Row, Col } from 'react-bootstrap';
66
import Button from '../../../widgets/TheButton';
77
import Callout from '../../../widgets/Callout';
88
import DateTime from '../../../widgets/DateTime';
9-
import Explanation from '../../../widgets/Explanation';
10-
11-
const syncMessages = {
12-
supplementaryFiles: (
13-
<FormattedMessage id="app.assignment.syncSupplementaryFiles" defaultMessage="Supplementary files" />
14-
),
15-
attachmentFiles: <FormattedMessage id="app.assignment.syncAttachmentFiles" defaultMessage="Text attachment files" />,
16-
exerciseTests: <FormattedMessage id="app.assignment.syncExerciseTests" defaultMessage="Exercise tests" />,
17-
localizedTexts: <FormattedMessage id="app.assignment.syncLocalizedTexts" defaultMessage="Localized texts" />,
18-
configurationType: (
19-
<FormattedMessage
20-
id="app.assignment.syncConfigurationType"
21-
defaultMessage="Configuration was switched to advanced mode"
22-
/>
23-
),
24-
scoreConfig: <FormattedMessage id="app.assignment.syncScoreConfig" defaultMessage="Score configuration" />,
25-
exerciseConfig: <FormattedMessage id="app.assignment.syncExerciseConfig" defaultMessage="Exercise configuration" />,
26-
runtimeEnvironments: (
27-
<FormattedMessage id="app.assignment.syncRuntimeEnvironments" defaultMessage="Selection of runtime environments" />
28-
),
29-
exerciseEnvironmentConfigs: (
30-
<FormattedMessage id="app.assignment.syncExerciseEnvironmentConfigs" defaultMessage="Environment configuration" />
31-
),
32-
hardwareGroups: <FormattedMessage id="app.assignment.syncHardwareGroups" defaultMessage="Hardware groups" />,
33-
limits: <FormattedMessage id="app.assignment.syncLimits" defaultMessage="Limits" />,
34-
mergeJudgeLogs: (
35-
<span>
36-
<FormattedMessage id="app.assignment.syncMergeJudgeLogs" defaultMessage="Judge logs merge flag" />
37-
<Explanation id="syncMergeJudgeLogs">
38-
<FormattedMessage
39-
id="app.exercise.mergeJudgeLogsExplanation"
40-
defaultMessage="The merge flag indicates whether primary (stdout) and secondary (stderr) judge logs are are concatenated in one log (which should be default for built-in judges). If the logs are separated, the visibility of each part may be controlled idividually in assignments. That might be helpful if you need to pass two separate logs from a custom judge (e.g., one is for students and one is for supervisors)."
41-
/>
42-
</Explanation>
43-
</span>
44-
),
45-
};
46-
47-
const getSyncMessages = syncInfo => {
48-
const res = [];
49-
for (const field in syncMessages) {
50-
if (!syncInfo[field]) {
51-
continue;
52-
}
53-
54-
if (!syncInfo[field].upToDate) {
55-
res.push(<li key={field}>{syncMessages[field]}</li>);
56-
}
57-
}
58-
return res;
59-
};
9+
import { getSyncMessages } from '../assignmentSyncHelper';
6010

6111
const AssignmentSync = ({ syncInfo, exerciseSync }) => {
6212
const messages = getSyncMessages(syncInfo);
@@ -68,16 +18,12 @@ const AssignmentSync = ({ syncInfo, exerciseSync }) => {
6818
<FormattedMessage
6919
id="app.assignment.syncRequiredTitle"
7020
defaultMessage="The exercise data are newer than assignment data"
71-
values={{
72-
exerciseUpdated: <DateTime unixts={syncInfo.updatedAt.exercise} emptyPlaceholder="??" />,
73-
assignmentUpdated: <DateTime unixts={syncInfo.updatedAt.assignment} emptyPlaceholder="??" />,
74-
}}
7521
/>
7622
</h4>
7723
<p>
7824
<FormattedMessage
7925
id="app.assignment.syncRequired"
80-
defaultMessage="Exercise was updated <strong>{exerciseUpdated}</strong>, but the assignment was last updated <strong>{assignmentUpdated}</strong>!"
26+
defaultMessage="Exercise was updated at <strong>{exerciseUpdated}</strong>, but the assignment was synchronized with the exercise at <strong>{assignmentUpdated}</strong>!"
8127
values={{
8228
exerciseUpdated: <DateTime unixts={syncInfo.updatedAt.exercise} emptyPlaceholder="??" />,
8329
assignmentUpdated: <DateTime unixts={syncInfo.updatedAt.assignment} emptyPlaceholder="??" />,
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import { FormattedMessage } from 'react-intl';
4+
import { OverlayTrigger, Popover } from 'react-bootstrap';
5+
6+
import DateTime from '../../../widgets/DateTime';
7+
import { WarningIcon } from '../../../icons';
8+
import { isUpToDate } from '../assignmentSyncHelper';
9+
10+
const AssignmentSyncIcon = ({ id, syncInfo, ...props }) => {
11+
return !isUpToDate(syncInfo) ? (
12+
<OverlayTrigger
13+
placement="bottom"
14+
overlay={
15+
<Popover id={`assginmentsync-${id}`}>
16+
<Popover.Title>
17+
<FormattedMessage
18+
id="app.assignment.syncRequiredTitle"
19+
defaultMessage="The exercise data are newer than assignment data"
20+
/>
21+
</Popover.Title>
22+
<Popover.Content>
23+
<p>
24+
<FormattedMessage
25+
id="app.assignment.syncRequired"
26+
defaultMessage="Exercise was updated at <strong>{exerciseUpdated}</strong>, but the assignment was synchronized with the exercise at <strong>{assignmentUpdated}</strong>!"
27+
values={{
28+
exerciseUpdated: <DateTime unixts={syncInfo.updatedAt.exercise} emptyPlaceholder="??" />,
29+
assignmentUpdated: <DateTime unixts={syncInfo.updatedAt.assignment} emptyPlaceholder="??" />,
30+
strong: contents => <strong>{contents}</strong>,
31+
}}
32+
/>
33+
</p>
34+
35+
{!syncInfo.isSynchronizationPossible && (
36+
<p>
37+
<WarningIcon className="text-danger" gapRight />
38+
<FormattedMessage
39+
id="app.assignment.syncIsNotPossible"
40+
defaultMessage="The exercise is not in a consistent state, synchronization is not possible at the moment."
41+
/>
42+
</p>
43+
)}
44+
</Popover.Content>
45+
</Popover>
46+
}>
47+
<WarningIcon {...props} className={syncInfo.isSynchronizationPossible ? 'text-warning' : 'text-danger'} />
48+
</OverlayTrigger>
49+
) : null;
50+
};
51+
52+
AssignmentSyncIcon.propTypes = {
53+
id: PropTypes.string.isRequired,
54+
syncInfo: PropTypes.object.isRequired,
55+
};
56+
57+
export default AssignmentSyncIcon;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import AssignmentSyncIcon from './AssignmentSyncIcon';
2+
export default AssignmentSyncIcon;

src/components/Assignments/Assignment/AssignmentTableRow/AssignmentTableRow.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ import { Link } from 'react-router-dom';
44
import { FormattedMessage, injectIntl } from 'react-intl';
55
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
66

7-
import AssignmentStatusIcon from '../AssignmentStatusIcon/AssignmentStatusIcon';
7+
import AssignmentSyncIcon from '../AssignmentSyncIcon';
8+
import AssignmentStatusIcon from '../AssignmentStatusIcon';
89
import AssignmentMaxPoints from '../AssignmentMaxPoints';
910
import withLinks from '../../../../helpers/withLinks';
1011
import { LocalizedExerciseName } from '../../../helpers/LocalizedNames';
1112
import { ChatIcon, EditIcon, ResultsIcon, MaybeBonusAssignmentIcon, MaybeVisibleAssignmentIcon } from '../../../icons';
1213
import DeleteAssignmentButtonContainer from '../../../../containers/DeleteAssignmentButtonContainer';
1314
import Button, { TheButtonGroup } from '../../../widgets/TheButton';
1415
import DateTime from '../../../widgets/DateTime';
16+
import NiceCheckbox from '../../../forms/NiceCheckbox';
1517
import EnvironmentsList from '../../../helpers/EnvironmentsList';
1618
import ResourceRenderer from '../../../helpers/ResourceRenderer';
1719
import { getGroupCanonicalLocalizedName } from '../../../../helpers/localizedData';
@@ -32,6 +34,7 @@ const AssignmentTableRow = ({
3234
visibleFrom,
3335
accepted,
3436
permissionHints,
37+
exerciseSynchronizationInfo,
3538
},
3639
runtimeEnvironments = null,
3740
status,
@@ -43,6 +46,8 @@ const AssignmentTableRow = ({
4346
showSecondDeadline = true,
4447
groupsAccessor = null,
4548
discussionOpen,
49+
setSelected = null,
50+
selected = false,
4651
intl: { locale },
4752
links: {
4853
ASSIGNMENT_DETAIL_URI_FACTORY,
@@ -53,13 +58,21 @@ const AssignmentTableRow = ({
5358
},
5459
}) => (
5560
<tr>
61+
{setSelected && (
62+
<td className="text-nowrap shrink-col">
63+
<NiceCheckbox name={id} checked={selected} onChange={setSelected} />
64+
</td>
65+
)}
66+
5667
<td className="text-nowrap shrink-col">
5768
{permissionHints.update || permissionHints.viewAssignmentSolutions ? (
5869
<MaybeVisibleAssignmentIcon id={id} isPublic={isPublic} visibleFrom={visibleFrom} />
5970
) : (
6071
<AssignmentStatusIcon id={id} status={status} accepted={accepted} />
6172
)}
6273
<MaybeBonusAssignmentIcon gapLeft id={id} isBonus={isBonus} />
74+
75+
{exerciseSynchronizationInfo && <AssignmentSyncIcon id={id} syncInfo={exerciseSynchronizationInfo} gapLeft />}
6376
</td>
6477

6578
{showNames && (
@@ -192,6 +205,7 @@ AssignmentTableRow.propTypes = {
192205
visibleFrom: PropTypes.number,
193206
accepted: PropTypes.bool,
194207
permissionHints: PropTypes.object,
208+
exerciseSynchronizationInfo: PropTypes.object,
195209
}).isRequired,
196210
runtimeEnvironments: PropTypes.array,
197211
status: PropTypes.string,
@@ -201,6 +215,8 @@ AssignmentTableRow.propTypes = {
201215
showNames: PropTypes.bool,
202216
showGroups: PropTypes.bool,
203217
showSecondDeadline: PropTypes.bool,
218+
setSelected: PropTypes.func,
219+
selected: PropTypes.bool,
204220
groupsAccessor: PropTypes.func,
205221
discussionOpen: PropTypes.func,
206222
links: PropTypes.object,

0 commit comments

Comments
 (0)