Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/course/assignments/assignments-panel.tsx
1503 views
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
import { Alert, Button, Col, Row } from "antd";
7
import { Map, Set } from "immutable";
8
import { FormattedMessage, useIntl } from "react-intl";
9
import {
10
AppRedux,
11
useActions,
12
useMemo,
13
useRedux,
14
useState,
15
} from "@cocalc/frontend/app-framework";
16
import { Gap, Icon, Tip } from "@cocalc/frontend/components";
17
import ScrollableList from "@cocalc/frontend/components/scrollable-list";
18
import { course } from "@cocalc/frontend/i18n";
19
import { cmp_array } from "@cocalc/util/misc";
20
21
import { CourseActions } from "../actions";
22
import { AddItems, FoldersToolbar } from "../common/folders-tool-bar";
23
import {
24
AssignmentRecord,
25
IsGradingMap,
26
NBgraderRunInfo,
27
SortDescription,
28
StudentRecord,
29
} from "../store";
30
import * as styles from "../styles";
31
import * as util from "../util";
32
import { Assignment } from "./assignment";
33
34
interface Props {
35
frame_id?: string;
36
name: string;
37
project_id: string;
38
redux: AppRedux;
39
actions: CourseActions;
40
assignments: Map<string, AssignmentRecord>;
41
students: Map<string, StudentRecord>;
42
user_map: object;
43
frameActions;
44
}
45
46
export function AssignmentsPanel(props: Props) {
47
const {
48
frame_id,
49
name,
50
project_id,
51
redux,
52
assignments,
53
students,
54
user_map,
55
frameActions,
56
} = props;
57
58
const intl = useIntl();
59
60
const course_actions = useActions<CourseActions>({ name });
61
62
const expanded_assignments: Set<string> = useRedux(
63
name,
64
"expanded_assignments",
65
);
66
const active_assignment_sort: SortDescription = useRedux(
67
name,
68
"active_assignment_sort",
69
);
70
const expanded_peer_configs: Set<string> = useRedux(
71
name,
72
"expanded_peer_configs",
73
);
74
const active_feedback_edits: IsGradingMap = useRedux(
75
name,
76
"active_feedback_edits",
77
);
78
const nbgrader_run_info: NBgraderRunInfo | undefined = useRedux(
79
name,
80
"nbgrader_run_info",
81
);
82
83
// search query to restrict which assignments are shown.
84
const pageFilter = useRedux(name, "pageFilter");
85
const filter = pageFilter?.get("assignments") ?? "";
86
const setFilter = (filter: string) => {
87
course_actions.setPageFilter("assignments", filter);
88
};
89
90
// whether or not to show deleted assignments on the bottom
91
const [show_deleted, set_show_deleted] = useState<boolean>(false);
92
93
function get_assignment(id: string): AssignmentRecord {
94
const assignment = assignments.get(id);
95
if (assignment == undefined) {
96
console.warn(`Tried to access undefined assignment ${id}`);
97
}
98
return assignment as any;
99
}
100
101
const { shown_assignments, num_omitted, num_deleted } = useMemo((): {
102
shown_assignments: any[];
103
num_omitted: number;
104
num_deleted: number;
105
} => {
106
let f, num_deleted, num_omitted;
107
let list = util.immutable_to_list(assignments, "assignment_id");
108
109
({ list, num_omitted } = util.compute_match_list({
110
list,
111
search_key: "path",
112
search: filter.trim(),
113
}));
114
115
if (active_assignment_sort.get("column_name") === "due_date") {
116
f = (a) => [
117
a.due_date != null ? a.due_date : 0,
118
a.path != null ? a.path.toLowerCase() : undefined,
119
];
120
} else if (active_assignment_sort.get("column_name") === "dir_name") {
121
f = (a) => [
122
a.path != null ? a.path.toLowerCase() : undefined,
123
a.due_date != null ? a.due_date : 0,
124
];
125
}
126
127
({ list, num_deleted } = util.order_list({
128
list,
129
compare_function: (a, b) => cmp_array(f(a), f(b)),
130
reverse: active_assignment_sort.get("is_descending"),
131
include_deleted: show_deleted,
132
}));
133
134
return {
135
shown_assignments: list,
136
num_omitted,
137
num_deleted,
138
};
139
}, [assignments, active_assignment_sort, show_deleted, filter]);
140
141
function render_sort_link(column_name: string, display_name: string) {
142
return (
143
<a
144
href=""
145
onClick={(e) => {
146
e.preventDefault();
147
return course_actions.assignments.set_active_assignment_sort(
148
column_name,
149
);
150
}}
151
>
152
{display_name}
153
<Gap />
154
{active_assignment_sort.get("column_name") === column_name ? (
155
<Icon
156
style={{ marginRight: "10px" }}
157
name={
158
active_assignment_sort.get("is_descending")
159
? "caret-up"
160
: "caret-down"
161
}
162
/>
163
) : undefined}
164
</a>
165
);
166
}
167
168
function render_assignment_table_header() {
169
return (
170
<div style={{ borderBottom: "1px solid #e5e5e5" }}>
171
<Row style={{ marginRight: "0px" }}>
172
<Col md={12}>
173
{render_sort_link(
174
"dir_name",
175
intl.formatMessage({
176
id: "course.assignments-panel.table-header.assignments",
177
defaultMessage: "Assignment Name",
178
}),
179
)}
180
</Col>
181
<Col md={12}>
182
{render_sort_link("due_date", intl.formatMessage(course.due_date))}
183
</Col>
184
</Row>
185
</div>
186
);
187
}
188
189
function render_assignment(assignment_id: string, index: number) {
190
return (
191
<Assignment
192
key={assignment_id}
193
project_id={project_id}
194
frame_id={frame_id}
195
name={name}
196
redux={redux}
197
assignment={get_assignment(assignment_id)}
198
background={index % 2 === 0 ? "#eee" : undefined}
199
students={students}
200
user_map={user_map}
201
is_expanded={expanded_assignments.has(assignment_id)}
202
expand_peer_config={expanded_peer_configs.has(assignment_id)}
203
active_feedback_edits={active_feedback_edits}
204
nbgrader_run_info={nbgrader_run_info}
205
/>
206
);
207
}
208
209
function render_assignments(assignments: { assignment_id: string }[]) {
210
if (assignments.length == 0) {
211
return render_no_assignments();
212
}
213
return (
214
<ScrollableList
215
virtualize
216
rowCount={assignments.length}
217
rowRenderer={({ key, index }) => render_assignment(key, index)}
218
rowKey={(index) => assignments[index]?.assignment_id ?? ""}
219
cacheId={`course-assignments-${name}-${frame_id}`}
220
/>
221
);
222
}
223
224
function render_no_assignments() {
225
return (
226
<div>
227
<Alert
228
type="info"
229
style={{
230
margin: "15px auto",
231
fontSize: "12pt",
232
maxWidth: "800px",
233
}}
234
message={
235
<b>
236
<a onClick={() => frameActions.setModal("add-assignments")}>
237
<FormattedMessage
238
id="course.assignments-panel.no_assignments.message"
239
defaultMessage={"Add Assignments to your Course"}
240
description={"online course for students"}
241
/>
242
</a>
243
</b>
244
}
245
description={
246
<div>
247
<FormattedMessage
248
id="course.assignments-panel.no_assignments.description"
249
defaultMessage={`
250
<p>
251
An assignment is a <i>directory</i> of files somewhere in your
252
CoCalc project. You copy the assignment to your students and
253
they work on it; later, you collect it, grade it, and return the
254
graded version to them.
255
</p>
256
<p>
257
<A>Add assignments to your course</A> by clicking "Add Assignment..." above.
258
You can create and select one or more directories and they will become assignments
259
that you can then customize and distribute to your students.
260
</p>`}
261
values={{
262
A: (c) => (
263
<a onClick={() => frameActions.setModal("add-assignments")}>
264
{c}
265
</a>
266
),
267
}}
268
description={"online course for students"}
269
/>
270
</div>
271
}
272
/>
273
</div>
274
);
275
}
276
277
function render_show_deleted(num_deleted: number, num_shown: number) {
278
if (show_deleted) {
279
return (
280
<Button
281
style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}
282
onClick={() => set_show_deleted(false)}
283
>
284
<Tip
285
placement="left"
286
title="Hide deleted"
287
tip="Assignments are never really deleted. Click this button so that deleted assignments aren't included at the bottom of the list. Deleted assignments are always hidden from the list of grades for a student."
288
>
289
Hide {num_deleted} deleted assignments
290
</Tip>
291
</Button>
292
);
293
} else {
294
return (
295
<Button
296
style={styles.show_hide_deleted({ needs_margin: num_shown > 0 })}
297
onClick={() => {
298
set_show_deleted(true);
299
setFilter("");
300
}}
301
>
302
<Tip
303
placement="left"
304
title="Show deleted"
305
tip="Assignments are not deleted forever even after you delete them. Click this button to show any deleted assignments at the bottom of the list of assignments. You can then click on the assignment and click undelete to bring the assignment back."
306
>
307
Show {num_deleted} deleted assignments
308
</Tip>
309
</Button>
310
);
311
}
312
}
313
314
function header() {
315
return (
316
<div style={{ marginBottom: "15px" }}>
317
<FoldersToolbar
318
search={filter}
319
search_change={setFilter}
320
num_omitted={num_omitted}
321
project_id={project_id}
322
items={assignments}
323
add_folders={course_actions.assignments.addAssignment}
324
item_name={"assignment"}
325
plural_item_name={"assignments"}
326
/>
327
</div>
328
);
329
}
330
331
return (
332
<div className={"smc-vfill"} style={{ margin: "0" }}>
333
{header()}
334
{shown_assignments.length > 0
335
? render_assignment_table_header()
336
: undefined}
337
<div className="smc-vfill">
338
{render_assignments(shown_assignments)}{" "}
339
{num_deleted
340
? render_show_deleted(num_deleted, shown_assignments.length)
341
: undefined}
342
</div>
343
</div>
344
);
345
}
346
347
// used for adding assignments outside of the above component.
348
export function AddAssignments({ name, actions, close }) {
349
const assignments = useRedux(name, "assignments");
350
return (
351
<AddItems
352
itemName="assignment"
353
items={assignments}
354
addItems={(paths) => {
355
actions.assignments.addAssignment(paths);
356
close?.();
357
}}
358
selectorStyle={{
359
position: null,
360
width: "100%",
361
boxShadow: null,
362
zIndex: null,
363
backgroundColor: null,
364
}}
365
defaultOpen
366
closable={false}
367
/>
368
);
369
}
370
371