CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

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