Skip to content

Commit f22cac7

Browse files
committed
Awarding shadow assignment points to multiple students simultaneously.
1 parent 6031830 commit f22cac7

File tree

5 files changed

+178
-21
lines changed

5 files changed

+178
-21
lines changed

src/components/Assignments/ShadowAssignmentPointsTable/ShadowAssignmentPointsTable.js

Lines changed: 138 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,32 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { FormattedMessage, injectIntl } from 'react-intl';
4-
5-
import { Table, Modal } from 'react-bootstrap';
4+
import { Field, reduxForm } from 'redux-form';
5+
import { Container, Row, Col, Table, Modal } from 'react-bootstrap';
66

77
import UsersNameContainer from '../../../containers/UsersNameContainer';
88
import EditShadowAssignmentPointsForm, {
99
getPointsFormInitialValues,
1010
transformPointsFormSubmitData,
1111
} from '../../forms/EditShadowAssignmentPointsForm';
1212
import Box from '../../widgets/Box';
13+
import Callout from '../../widgets/Callout';
14+
import { TextField, NumericTextField, SimpleCheckboxField } from '../../forms/Fields';
15+
import SubmitButton from '../../forms/SubmitButton';
1316
import DateTime from '../../widgets/DateTime';
1417
import Button, { TheButtonGroup } from '../../widgets/TheButton';
1518
import Confirm from '../../forms/Confirm';
16-
import Icon, { EditIcon, DeleteIcon } from '../../icons';
19+
import Icon, { BanIcon, EditIcon, DeleteIcon, SaveIcon } from '../../icons';
1720
import { createUserNameComparator } from '../../helpers/users';
1821
import { arrayToObject, safeGet } from '../../../helpers/common';
1922
import withLinks from '../../../helpers/withLinks';
2023

2124
class ShadowAssignmentPointsTable extends Component {
22-
state = { dialogStudentId: null, dialogPointsId: null };
25+
state = { dialogStudentId: null, dialogPointsId: null, multiAwardMode: false };
26+
27+
toggleMultiAwardMode = () => {
28+
this.setState({ multiAwardMode: !this.state.multiAwardMode });
29+
};
2330

2431
openDialog = (studentId, pointsId = null) =>
2532
this.setState({
@@ -50,6 +57,14 @@ class ShadowAssignmentPointsTable extends Component {
5057
points,
5158
permissionHints,
5259
maxPoints,
60+
submitting,
61+
handleSubmit,
62+
onSubmit,
63+
dirty,
64+
submitFailed = false,
65+
submitSucceeded = false,
66+
invalid,
67+
warning,
5368
intl: { locale },
5469
links: { GROUP_USER_SOLUTIONS_URI_FACTORY },
5570
} = this.props;
@@ -61,14 +76,82 @@ class ShadowAssignmentPointsTable extends Component {
6176
title={
6277
<FormattedMessage id="app.shadowAssignmentPointsTable.title" defaultMessage="Shadow Assignment Points" />
6378
}
64-
collapsable
6579
isOpen
6680
noPadding
67-
unlimitedHeight>
81+
unlimitedHeight
82+
footer={
83+
permissionHints.createPoints ? (
84+
this.state.multiAwardMode ? (
85+
<>
86+
<Container fluid>
87+
<Row>
88+
<Col>
89+
<NumericTextField
90+
name="points"
91+
maxLength={6}
92+
validateMin={-10000}
93+
validateMax={10000}
94+
label={
95+
<FormattedMessage id="app.editShadowAssignmentPointsForm.points" defaultMessage="Points:" />
96+
}
97+
/>
98+
</Col>
99+
100+
<Col lg={9}>
101+
<Field
102+
name="note"
103+
component={TextField}
104+
maxLength={1024}
105+
label={<FormattedMessage id="app.editShadowAssignmentPointsForm.note" defaultMessage="Note:" />}
106+
/>
107+
</Col>
108+
</Row>
109+
</Container>
110+
111+
{warning && <Callout variant="warning">{warning}</Callout>}
112+
113+
<div className="text-center text-nowrap mb-1">
114+
<TheButtonGroup>
115+
<SubmitButton
116+
id="multi-assign-form"
117+
handleSubmit={handleSubmit(data => onSubmit(data).then(this.toggleMultiAwardMode))}
118+
submitting={submitting}
119+
dirty={dirty}
120+
hasSucceeded={submitSucceeded}
121+
hasFailed={submitFailed}
122+
invalid={invalid}
123+
defaultIcon={<SaveIcon gapRight />}
124+
messages={{
125+
submit: <FormattedMessage id="generic.save" defaultMessage="Save" />,
126+
submitting: <FormattedMessage id="generic.saving" defaultMessage="Saving..." />,
127+
success: <FormattedMessage id="generic.saved" defaultMessage="Saved" />,
128+
}}
129+
/>
130+
<Button variant="danger" onClick={this.toggleMultiAwardMode}>
131+
<BanIcon gapRight />
132+
<FormattedMessage id="generic.cancel" defaultMessage="Cancel" />
133+
</Button>
134+
</TheButtonGroup>
135+
</div>
136+
</>
137+
) : (
138+
<div className="text-center">
139+
<Button variant="primary" onClick={this.toggleMultiAwardMode}>
140+
<Icon gapRight icon={['far', 'check-square']} />
141+
<FormattedMessage
142+
id="app.shadowAssignmentPointsTable.multiAwardButton"
143+
defaultMessage="Award Points Collectively"
144+
/>
145+
</Button>
146+
</div>
147+
)
148+
) : null
149+
}>
68150
<>
69-
<Table responsive hover>
151+
<Table responsive hover className="mb-1">
70152
<thead>
71153
<tr>
154+
{this.state.multiAwardMode && <th />}
72155
<th>
73156
<FormattedMessage id="app.shadowAssignmentPointsTable.user" defaultMessage="User" />
74157
</th>
@@ -91,6 +174,13 @@ class ShadowAssignmentPointsTable extends Component {
91174
const awardedAt = safeGet(studentPoints, [student.id, 'awardedAt'], null);
92175
return (
93176
<tr key={student.id}>
177+
{this.state.multiAwardMode && (
178+
<td className="shrink-col">
179+
{points === null && permissionHints.createPoints && (
180+
<Field name={`students.${student.id}`} component={SimpleCheckboxField} />
181+
)}
182+
</td>
183+
)}
94184
<td className="text-nowrap">
95185
<UsersNameContainer
96186
userId={student.id}
@@ -101,20 +191,18 @@ class ShadowAssignmentPointsTable extends Component {
101191
<td className="text-center text-nowrap">{points !== null ? points : <span>&mdash;</span>}</td>
102192
<td>{awardedAt && <DateTime unixts={awardedAt} showRelative />}</td>
103193
<td>{safeGet(studentPoints, [student.id, 'note'], null)}</td>
104-
{points === null ? (
105-
<td className="shrink-col text-nowrap text-right">
106-
{permissionHints.createPoints && (
194+
<td className="shrink-col text-nowrap text-right">
195+
{points === null ? (
196+
permissionHints.createPoints && (
107197
<Button variant="success" onClick={() => this.openDialog(student.id)} size="xs">
108198
<Icon gapRight icon={['far', 'star']} />
109199
<FormattedMessage
110200
id="app.shadowAssignmentPointsTable.createPointsButton"
111201
defaultMessage="Award Points"
112202
/>
113203
</Button>
114-
)}
115-
</td>
116-
) : (
117-
<td className="shrink-col text-nowrap text-right">
204+
)
205+
) : (
118206
<TheButtonGroup>
119207
{permissionHints.updatePoints && (
120208
<Button variant="warning" onClick={() => this.openDialog(student.id, pointsId)} size="xs">
@@ -143,8 +231,8 @@ class ShadowAssignmentPointsTable extends Component {
143231
</Confirm>
144232
)}
145233
</TheButtonGroup>
146-
</td>
147-
)}
234+
)}
235+
</td>
148236
</tr>
149237
);
150238
})}
@@ -187,8 +275,41 @@ ShadowAssignmentPointsTable.propTypes = {
187275
maxPoints: PropTypes.number.isRequired,
188276
setPoints: PropTypes.func.isRequired,
189277
removePoints: PropTypes.func.isRequired,
278+
handleSubmit: PropTypes.func.isRequired,
279+
onSubmit: PropTypes.func.isRequired,
280+
submitFailed: PropTypes.bool,
281+
dirty: PropTypes.bool,
282+
submitSucceeded: PropTypes.bool,
283+
submitting: PropTypes.bool,
284+
invalid: PropTypes.bool,
285+
warning: PropTypes.any,
190286
intl: PropTypes.object.isRequired,
191287
links: PropTypes.object,
192288
};
193289

194-
export default withLinks(injectIntl(ShadowAssignmentPointsTable));
290+
const warn = ({ points }, { maxPoints }) => {
291+
const warnings = {};
292+
293+
if (maxPoints > 0) {
294+
if (points > maxPoints || points < 0) {
295+
warnings._warning = (
296+
<FormattedMessage
297+
id="app.editShadowAssignmentPointsForm.validation.pointsOutOfRange"
298+
defaultMessage="Points are out of regular range. Regular score for this assignment is between 0 and {maxPoints}."
299+
values={{ maxPoints }}
300+
/>
301+
);
302+
}
303+
}
304+
305+
return warnings;
306+
};
307+
308+
export default withLinks(
309+
reduxForm({
310+
form: 'multi-assign-form',
311+
enableReinitialize: true,
312+
keepDirtyOnReinitialize: false,
313+
warn,
314+
})(injectIntl(ShadowAssignmentPointsTable))
315+
);

src/components/Exercises/ReferenceSolutionsTable/ReferenceSolutionsTableRow.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const ReferenceSolutionsTableRow = ({
6666
<Tooltip id="failure">
6767
{lastSubmission.failure.description || (
6868
<FormattedMessage
69-
id="app.referenceSolutionTable.stillEvaluating"
69+
id="app.referenceSolutionTable.evaluationFailed"
7070
defaultMessage="Last evaluation failed"
7171
/>
7272
)}

src/containers/ShadowAssignmentPointsContainer/ShadowAssignmentPointsContainer.js

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,44 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33
import { connect } from 'react-redux';
4+
import { defaultMemoize } from 'reselect';
5+
import moment from 'moment';
46

57
import ShadowAssignmentPointsTable from '../../components/Assignments/ShadowAssignmentPointsTable';
68

79
import { fetchGroupIfNeeded } from '../../redux/modules/groups';
810
import { fetchByIds } from '../../redux/modules/users';
9-
import { safeGet } from '../../helpers/common';
1011
import { setShadowAssignmentPoints, removeShadowAssignmentPoints } from '../../redux/modules/shadowAssignments';
1112
import { studentsOfGroupSelector } from '../../redux/selectors/usersGroups';
13+
import { safeGet, arrayToObject } from '../../helpers/common';
14+
15+
const prepareInitialValues = defaultMemoize((students, maxPoints) => ({
16+
points: maxPoints,
17+
note: '',
18+
students: Object.fromEntries(students.map(({ id }) => [id, false])),
19+
}));
1220

1321
class ShadowAssignmentPointsContainer extends Component {
22+
multiAssignSubmit = data => {
23+
const pointsId = null;
24+
const awardedAt = moment().startOf('minute').unix();
25+
const studentPoints = arrayToObject(this.props.points, ({ awardeeId }) => awardeeId);
26+
const { note, points, students } = data;
27+
return Promise.all(
28+
this.props.students
29+
.filter(({ id }) => safeGet(studentPoints, [id, 'points'], null) === null && students[id])
30+
.map(({ id }) =>
31+
this.props.setPoints({
32+
awardeeId: id, // only the student ID changes among setPoint calls
33+
pointsId,
34+
points,
35+
note,
36+
awardedAt,
37+
})
38+
)
39+
);
40+
};
41+
1442
componentDidMount = () => this.props.loadAsync();
1543

1644
componentDidUpdate(prevProps) {
@@ -30,6 +58,8 @@ class ShadowAssignmentPointsContainer extends Component {
3058
permissionHints={permissionHints}
3159
setPoints={setPoints}
3260
removePoints={removePoints}
61+
initialValues={prepareInitialValues(students, maxPoints)}
62+
onSubmit={this.multiAssignSubmit}
3363
/>
3464
);
3565
}

src/locales/cs.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,8 +1195,9 @@
11951195
"app.referenceSolution.title": "Podrobnosti referenčního řešení",
11961196
"app.referenceSolutionDetail.exercise": "Úloha",
11971197
"app.referenceSolutionDetail.title.details": "Detail referenčního řešení",
1198+
"app.referenceSolutionTable.evaluationFailed": "Poslední vyhodnocení selhalo",
11981199
"app.referenceSolutionTable.noDescription": "popis nebyl uveden",
1199-
"app.referenceSolutionTable.stillEvaluating": "Poslední vyhodnocení selhalo",
1200+
"app.referenceSolutionTable.stillEvaluating": "Řešení se stále vyhodnocuje",
12001201
"app.registration.external.gotoSignin": "Stránka přihlášení",
12011202
"app.registration.external.link": "Navštívit stránky podpory",
12021203
"app.registration.external.mail": "Kontaktovat podporu",
@@ -1354,6 +1355,7 @@
13541355
"app.shadowAssignmentPointsTable.awardedAt": "Ohodnoceno",
13551356
"app.shadowAssignmentPointsTable.createPointsButton": "Ohodnotit",
13561357
"app.shadowAssignmentPointsTable.formModalTitle": "Přiřadit body za stínovou úlohu",
1358+
"app.shadowAssignmentPointsTable.multiAwardButton": "Ohodnotit hromadně",
13571359
"app.shadowAssignmentPointsTable.note": "Poznámka",
13581360
"app.shadowAssignmentPointsTable.receivedPoints": "Body",
13591361
"app.shadowAssignmentPointsTable.removePointsButtonConfirmation": "Opravdu si přejete odebrat přidělené body?",
@@ -1620,6 +1622,7 @@
16201622
"generic.acknowledge": "Beru na vědomí",
16211623
"generic.assignedAt": "Zadáno",
16221624
"generic.author": "Autor",
1625+
"generic.cancel": "Zrušit",
16231626
"generic.clearAll": "Zrušit vše",
16241627
"generic.close": "Zavřít",
16251628
"generic.correctness": "Správnost",

src/locales/en.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,8 +1195,9 @@
11951195
"app.referenceSolution.title": "Reference Solution Detail",
11961196
"app.referenceSolutionDetail.exercise": "Exercise",
11971197
"app.referenceSolutionDetail.title.details": "Reference Solution Detail",
1198+
"app.referenceSolutionTable.evaluationFailed": "Last evaluation failed",
11981199
"app.referenceSolutionTable.noDescription": "no description given",
1199-
"app.referenceSolutionTable.stillEvaluating": "Last evaluation failed",
1200+
"app.referenceSolutionTable.stillEvaluating": "Last submission is still evaluating",
12001201
"app.registration.external.gotoSignin": "Sign-in Page",
12011202
"app.registration.external.link": "Visit Help Page",
12021203
"app.registration.external.mail": "Contact Support",
@@ -1354,6 +1355,7 @@
13541355
"app.shadowAssignmentPointsTable.awardedAt": "Awarded At",
13551356
"app.shadowAssignmentPointsTable.createPointsButton": "Award Points",
13561357
"app.shadowAssignmentPointsTable.formModalTitle": "Set Shadow Assignment Points",
1358+
"app.shadowAssignmentPointsTable.multiAwardButton": "Award Points Collectively",
13571359
"app.shadowAssignmentPointsTable.note": "Note",
13581360
"app.shadowAssignmentPointsTable.receivedPoints": "Points",
13591361
"app.shadowAssignmentPointsTable.removePointsButtonConfirmation": "Do you really wish to remove awarded points?",
@@ -1620,6 +1622,7 @@
16201622
"generic.acknowledge": "Acknowledge",
16211623
"generic.assignedAt": "Assigned at",
16221624
"generic.author": "Author",
1625+
"generic.cancel": "Cancel",
16231626
"generic.clearAll": "Clear All",
16241627
"generic.close": "Close",
16251628
"generic.correctness": "Correctness",

0 commit comments

Comments
 (0)