Skip to content

Commit a055aab

Browse files
committed
course peer grading: parse and enter grades student grades
1 parent 5ee0c95 commit a055aab

File tree

5 files changed

+170
-43
lines changed

5 files changed

+170
-43
lines changed

src/packages/frontend/course/actions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -266,11 +266,11 @@ export class CourseActions extends Actions<CourseState> {
266266
}
267267

268268
// ACTIVITY ACTIONS
269-
public set_activity(
269+
public set_activity = (
270270
opts: { id: number; desc?: string } | { id?: number; desc: string },
271-
): number {
271+
): number => {
272272
return this.activity.set_activity(opts);
273-
}
273+
};
274274

275275
public clear_activity(id?: number): void {
276276
this.activity.clear_activity(id);

src/packages/frontend/course/assignments/actions.ts

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,10 @@ import {
6565
NBGRADER_MAX_OUTPUT,
6666
NBGRADER_MAX_OUTPUT_PER_CELL,
6767
NBGRADER_TIMEOUT_MS,
68-
PEER_GRADING_GUIDE_FN,
68+
PEER_GRADING_GUIDE_FILENAME,
6969
STUDENT_SUBDIR,
70+
PEER_GRADING_GUIDELINES_GRADE_MARKER,
71+
PEER_GRADING_GUIDELINES_COMMENT_MARKER,
7072
} from "./consts";
7173

7274
export class AssignmentsActions {
@@ -891,10 +893,10 @@ ${details}
891893
});
892894
this.course_actions.student_projects.action_all_student_projects("start");
893895
// We request to start all projects simultaneously, and the system
894-
// will start doing that. I think it's no so much important that
896+
// will start doing that. I think it's not so much important that
895897
// the projects are actually running, but that they were started
896898
// before the copy operations started.
897-
await delay(15 * 1000);
899+
await delay(5 * 1000);
898900
this.course_actions.clear_activity(id);
899901
}
900902

@@ -948,8 +950,120 @@ ${details}
948950
desc,
949951
short_desc,
950952
);
953+
await this.peerParseStudentGrading(assignment_id);
951954
}
952955

956+
private peerParseStudentGrading = async (assignment_id: string) => {
957+
// For each student do the following:
958+
// If they already have a recorded grade, do nothing further.
959+
// If they do not have a recorded grade, load all of the
960+
// PEER_GRADING_GUIDE_FILENAME files that were collected
961+
// from the students, then create a grade from that (if possible), along
962+
// with a comment that explains how that grade was obtained, without
963+
// saying which student did what.
964+
const { store, assignment } = this.course_actions.resolve({
965+
assignment_id,
966+
});
967+
if (assignment == null) {
968+
throw Error("no such assignment");
969+
}
970+
const id = this.course_actions.set_activity({
971+
desc: "Parsing peer grading",
972+
});
973+
const allGrades = assignment.get("grades", Map()).toJS() as {
974+
[student_id: string]: string;
975+
};
976+
const allComments = assignment.get("comments", Map()).toJS() as {
977+
[student_id: string]: string;
978+
};
979+
// compute missing grades
980+
for (const student_id of store.get_student_ids()) {
981+
if (allGrades[student_id]) {
982+
// a grade is already set
983+
continue;
984+
}
985+
// attempt to compute a grade
986+
const peer_student_ids: string[] = store.get_peers_that_graded_student(
987+
assignment_id,
988+
student_id,
989+
);
990+
const course_project_id = store.get("course_project_id");
991+
const grades: number[] = [];
992+
let comments: string[] = [];
993+
const student_name = store.get_student_name(student_id);
994+
this.course_actions.set_activity({
995+
id,
996+
desc: `Parsing peer grading of ${student_name}`,
997+
});
998+
for (const peer_student_id of peer_student_ids) {
999+
const path = join(
1000+
`${assignment.get("collect_path")}-peer-grade`,
1001+
student_id,
1002+
peer_student_id,
1003+
PEER_GRADING_GUIDE_FILENAME,
1004+
);
1005+
try {
1006+
const contents = await webapp_client.project_client.read_text_file({
1007+
project_id: course_project_id,
1008+
path,
1009+
});
1010+
const i = contents.lastIndexOf(PEER_GRADING_GUIDELINES_GRADE_MARKER);
1011+
if (i == -1) {
1012+
continue;
1013+
}
1014+
let j = contents.lastIndexOf(PEER_GRADING_GUIDELINES_COMMENT_MARKER);
1015+
if (j == -1) {
1016+
j = contents.length;
1017+
}
1018+
const grade = parseFloat(
1019+
contents
1020+
.slice(i + PEER_GRADING_GUIDELINES_GRADE_MARKER.length, j)
1021+
.trim(),
1022+
);
1023+
if (!isFinite(grade) && isNaN(grade)) {
1024+
continue;
1025+
}
1026+
const comment = contents.slice(
1027+
j + PEER_GRADING_GUIDELINES_COMMENT_MARKER.length,
1028+
);
1029+
grades.push(grade);
1030+
comments.push(comment);
1031+
} catch (err) {
1032+
// grade not available for some reason
1033+
console.warn("issue reading peer grading file", {
1034+
path,
1035+
err,
1036+
student_name,
1037+
});
1038+
}
1039+
}
1040+
if (grades.length > 0) {
1041+
const grade = grades.reduce((a, b) => a + b) / grades.length;
1042+
allGrades[student_id] = `${grade}`;
1043+
if (!allComments[student_id]) {
1044+
const studentComments = comments
1045+
.filter((x) => x.trim())
1046+
.map((x) => `- ${x}`)
1047+
.join("\n\n");
1048+
allComments[student_id] = `Grades: ${grades.join(", ")}\n\n${
1049+
studentComments ? "Student Comments:\n" + studentComments : ""
1050+
}`;
1051+
}
1052+
}
1053+
}
1054+
// set them in the course data
1055+
this.course_actions.set(
1056+
{
1057+
table: "assignments",
1058+
assignment_id,
1059+
grades: allGrades,
1060+
comments: allComments,
1061+
},
1062+
true,
1063+
);
1064+
this.course_actions.clear_activity(id);
1065+
};
1066+
9531067
private async assignment_action_all_students(
9541068
assignment_id: string,
9551069
new_only: boolean,
@@ -1094,7 +1208,7 @@ ${details}
10941208
// write instructions file for the student, where they enter the grade,
10951209
// and also it tells them what to do.
10961210
await this.write_text_file_to_course_project({
1097-
path: join(src_path, PEER_GRADING_GUIDE_FN),
1211+
path: join(src_path, PEER_GRADING_GUIDE_FILENAME),
10981212
content: guidelines,
10991213
});
11001214
const target_path = join(target_base_path, peer_student_id);
@@ -1125,10 +1239,10 @@ ${details}
11251239

11261240
// Collect all the peer graading of the given student (not the work the student did, but
11271241
// the grading about the student!).
1128-
private async peer_collect_from_student(
1242+
private peer_collect_from_student = async (
11291243
assignment_id: string,
11301244
student_id: string,
1131-
): Promise<void> {
1245+
): Promise<void> => {
11321246
if (this.start_copy(assignment_id, student_id, "last_peer_collect")) {
11331247
return;
11341248
}
@@ -1215,7 +1329,7 @@ ${details}
12151329
} catch (err) {
12161330
finish(err);
12171331
}
1218-
}
1332+
};
12191333

12201334
// This doesn't really stop it yet, since that's not supported by the backend.
12211335
// It does stop the spinner and let the user try to restart the copy.

src/packages/frontend/course/assignments/configure-peer.tsx

Lines changed: 16 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -17,26 +17,11 @@ import { server_days_ago } from "@cocalc/util/misc";
1717
import { Button, Card, Col, InputNumber, Row, Switch, Typography } from "antd";
1818
import { CourseActions } from "../actions";
1919
import { AssignmentRecord } from "../store";
20-
import { PEER_GRADING_GUIDE_FN } from "./consts";
21-
22-
// everything from GRADING_GUIDELINES_GRADE_MARKER to GRADING_GUIDELINES_COMMENT_MARKER
23-
// is parsed as a numerical grade, if possible. i18n's don't mess this up! Also,
24-
// changing this would break outstanding assignments, so change with caution.
25-
const GRADING_GUIDELINES_GRADE_MARKER = "OVERALL GRADE (A SINGLE NUMBER):";
26-
const GRADING_GUIDELINES_COMMENT_MARKER = "COMMENTS ABOUT GRADE:";
27-
28-
const DEFAULT_GUIDELINES = `
29-
Put your final overall score below after "${GRADING_GUIDELINES_GRADE_MARKER}"
30-
31-
32-
33-
${GRADING_GUIDELINES_GRADE_MARKER}
34-
35-
36-
37-
${GRADING_GUIDELINES_COMMENT_MARKER}
38-
39-
`;
20+
import {
21+
PEER_GRADING_GUIDE_FILENAME,
22+
PEER_GRADING_GUIDELINES_GRADE_MARKER,
23+
PEER_GRADING_DEFAULT_GUIDELINES,
24+
} from "./consts";
4025

4126
interface Props {
4227
assignment: AssignmentRecord;
@@ -78,8 +63,8 @@ export const ConfigurePeerGrading: React.FC<Props> = React.memo(
7863
}
7964

8065
function set_peer_grade(config) {
81-
if (config.enabled && !config.guidelines) {
82-
config.guidelines = DEFAULT_GUIDELINES;
66+
if (config.enabled && !config.guidelines?.trim()) {
67+
config.guidelines = PEER_GRADING_DEFAULT_GUIDELINES;
8368
}
8469
actions.assignments.set_peer_grade(
8570
assignment.get("assignment_id"),
@@ -152,15 +137,15 @@ export const ConfigurePeerGrading: React.FC<Props> = React.memo(
152137
Grading guidelines:{" "}
153138
<Typography.Text type={"secondary"}>
154139
This text will be made available to students in their grading
155-
folder in a file <code>{PEER_GRADING_GUIDE_FN}</code>. Tell your
156-
students how to grade each problem. Since this is a markdown
157-
file, you might also provide a link to a publicly shared file or
158-
directory with additional guidelines. If you keep the default "
159-
{GRADING_GUIDELINES_GRADE_MARKER}" text, then the grade the
160-
student puts after that will be parsed from the file and made
161-
available to you in the user interface, and it can impact the
162-
default grade assigned to the student (e.g., if it is numerical,
163-
then the average is assigned).
140+
folder in a file <code>{PEER_GRADING_GUIDE_FILENAME}</code>.
141+
Tell your students how to grade each problem. Since this is a
142+
markdown file, you might also provide a link to a publicly
143+
shared file or directory with additional guidelines. If you keep
144+
the default "{PEER_GRADING_GUIDELINES_GRADE_MARKER}" text, then
145+
the grade the student puts after that will be parsed from the
146+
file and the default grade assigned to the student will be the
147+
average of the peer grades. You can edit the grade before
148+
returning the graded assignment to the student.
164149
</Typography.Text>
165150
</Typography.Paragraph>
166151
</Col>

src/packages/frontend/course/assignments/consts.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,30 @@ export const NBGRADER_MAX_OUTPUT_PER_CELL: number = 500000;
1616
export const NBGRADER_MAX_OUTPUT: number = 4000000;
1717

1818
// filename of the peer grading guide
19-
export const PEER_GRADING_GUIDE_FN = "GRADING-GUIDE.md";
19+
export const PEER_GRADING_GUIDE_FILENAME = "GRADING-GUIDE.md";
20+
21+
// Everything from GRADING_GUIDELINES_GRADE_MARKER to GRADING_GUIDELINES_COMMENT_MARKER
22+
// is parsed as a numerical grade, if possible. i18n's don't mess this up! Also,
23+
// changing this would break outstanding assignments, so change with caution.
24+
// A fix
25+
// would be to store these strings somewhere when pushing the assignment out, so that
26+
// the same ones are used when collecting and parsing. But that will take a few hours
27+
// more work, and it is only necessary if we decide to change these. Whoever decides
28+
// to change these has to do that work.
29+
export const PEER_GRADING_GUIDELINES_GRADE_MARKER =
30+
"OVERALL GRADE (a single number):";
31+
export const PEER_GRADING_GUIDELINES_COMMENT_MARKER =
32+
"COMMENTS ABOUT GRADE (student will see, but not who made them):";
33+
34+
export const PEER_GRADING_DEFAULT_GUIDELINES = `
35+
Put your final overall score below after "${PEER_GRADING_GUIDELINES_GRADE_MARKER}"
36+
37+
38+
39+
${PEER_GRADING_GUIDELINES_GRADE_MARKER}
40+
41+
42+
43+
${PEER_GRADING_GUIDELINES_COMMENT_MARKER}
44+
45+
`;

src/packages/frontend/frame-editors/course-editor/course-panel-wrapper.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,9 @@ const CoursePanelWrapper: React.FC<FrameProps> = React.memo(
182182
trunc={80}
183183
on_clear={() => {
184184
const actions = redux.getActions(name) as CourseActions;
185-
if (actions != null) actions.clear_activity();
185+
if (actions != null) {
186+
actions.clear_activity();
187+
}
186188
}}
187189
/>
188190
);

0 commit comments

Comments
 (0)