Skip to content

courses: add exams and group assignments #7888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/STYLE.md
Original file line number Diff line number Diff line change
@@ -14,6 +14,11 @@

- NOTE: there's a lot of Javascript code in cocalc that uses Python conventions. Long ago Nicholas R. argued "by using Python conventions we can easily distinguish our code from other code"; in retrospect, this was a bad argument, and only serves to make Javascript devs less comfortable in our codebase, and make our code look weird compared to most Javascript code. Rewrite it.

- Abbreviations: Do not use obscure abbreviations for variable names.

- Good code is read much more than it is written, so make it easy to read.
- E.g., do not use "dflt" since: (1) it barely saves any characters over "default", and (2) if you do a Google search for "dflt" you will see it's not even a common abbreviation for default.

- Javascript Methods: Prefer arrow functions for methods of classes.

- it's standard
@@ -79,4 +84,3 @@ const MyButton: React.FC<MyButtonProps> = (props) => {
- Bootstrap:
- CoCalc used to use jquery + bootstrap (way before react even existed!) for everything, and that's still in use for some things today (e.g., Sage Worksheets). Rewrite or delete all this.
- CoCalc also used to use react-bootstrap, and sadly still does. Get rid of this.

140 changes: 101 additions & 39 deletions src/packages/frontend/course/assignments/actions.ts
Original file line number Diff line number Diff line change
@@ -37,7 +37,7 @@ import {
} from "@cocalc/util/misc";
import { delay, map } from "awaiting";
import { debounce } from "lodash";
import { Map } from "immutable";
import { Map as iMap } from "immutable";
import { CourseActions } from "../actions";
import { export_assignment } from "../export/export-assignment";
import { export_student_file_use_times } from "../export/file-use-times";
@@ -47,6 +47,7 @@ import {
CourseStore,
get_nbgrader_score,
NBgraderRunInfo,
AssignmentLocation,
} from "../store";
import {
AssignmentCopyType,
@@ -72,6 +73,7 @@ import {
DUE_DATE_FILENAME,
} from "./consts";
import { COPY_TIMEOUT_MS } from "../consts";
import { getLocation } from "./location";

const UPDATE_DUE_DATE_FILENAME_DEBOUNCE_MS = 3000;

@@ -214,7 +216,7 @@ export class AssignmentsActions {
}
// Annoying that we have to convert to JS here and cast,
// but the set below seems to require it.
let grades = assignment.get("grades", Map()).toJS() as {
let grades = assignment.get("grades", iMap()).toJS() as {
[student_id: string]: string;
};
grades[student_id] = grade;
@@ -243,7 +245,7 @@ export class AssignmentsActions {
}
// Annoying that we have to convert to JS here and cast,
// but the set below seems to require it.
let comments = assignment.get("comments", Map()).toJS() as {
let comments = assignment.get("comments", iMap()).toJS() as {
[student_id: string]: string;
};
comments[student_id] = comment;
@@ -356,14 +358,11 @@ export class AssignmentsActions {
});
if (!student || !assignment) return;
const content = this.dueDateFileContent(assignment_id);
const project_id = student.get("project_id");
if (!project_id) return;
const project_id = this.getProjectId({ assignment, student });
if (!project_id) {
return;
}
const path = join(assignment.get("target_path"), DUE_DATE_FILENAME);
console.log({
project_id,
path,
content,
});
await webapp_client.project_client.write_text_file({
project_id,
path,
@@ -441,12 +440,6 @@ export class AssignmentsActions {
});
if (!student || !assignment) return;
const student_name = store.get_student_name(student_id);
const student_project_id = student.get("project_id");
if (student_project_id == null) {
// nothing to do
this.course_actions.clear_activity(id);
return;
}
const target_path = join(
assignment.get("collect_path"),
student.get("student_id"),
@@ -456,6 +449,15 @@ export class AssignmentsActions {
desc: `Copying assignment from ${student_name}`,
});
try {
const student_project_id = this.getProjectId({
assignment,
student,
});
if (student_project_id == null) {
// nothing to do
this.course_actions.clear_activity(id);
return;
}
await webapp_client.project_client.copy_path_between_projects({
src_project_id: student_project_id,
src_path: assignment.get("target_path"),
@@ -512,7 +514,7 @@ export class AssignmentsActions {
const grade = store.get_grade(assignment_id, student_id);
const comments = store.get_comments(assignment_id, student_id);
const student_name = store.get_student_name(student_id);
const student_project_id = student.get("project_id");
const student_project_id = this.getProjectId({ assignment, student });

// if skip_grading is true, this means there *might* no be a "grade" given,
// but instead some grading inside the files or an external tool is used.
@@ -828,22 +830,12 @@ ${details}
id,
desc: `Copying assignment to ${student_name}`,
});
let student_project_id: string | undefined = student.get("project_id");
const src_path = this.assignment_src_path(assignment);
try {
if (student_project_id == null) {
this.course_actions.set_activity({
id,
desc: `${student_name}'s project doesn't exist, so creating it.`,
});
student_project_id =
await this.course_actions.student_projects.create_student_project(
student_id,
);
if (!student_project_id) {
throw Error("failed to create project");
}
}
const student_project_id = await this.getOrCreateProjectId({
assignment,
student,
});
if (create_due_date_file) {
await this.copy_assignment_create_due_date_file(assignment_id);
}
@@ -1091,10 +1083,10 @@ ${details}
const id = this.course_actions.set_activity({
desc: "Parsing peer grading",
});
const allGrades = assignment.get("grades", Map()).toJS() as {
const allGrades = assignment.get("grades", iMap()).toJS() as {
[student_id: string]: string;
};
const allComments = assignment.get("comments", Map()).toJS() as {
const allComments = assignment.get("comments", iMap()).toJS() as {
[student_id: string]: string;
};
// compute missing grades
@@ -1328,7 +1320,10 @@ ${details}
return;
}

const student_project_id = student.get("project_id");
const student_project_id = this.getProjectId({
assignment,
student,
});
if (!student_project_id) {
finish();
return;
@@ -1499,7 +1494,7 @@ ${details}
student_id,
});
if (assignment == null || student == null) return;
const student_project_id = student.get("project_id");
const student_project_id = this.getProjectId({ assignment, student });
if (student_project_id == null) {
this.course_actions.set_error(
"open_assignment: student project not yet created",
@@ -1800,7 +1795,7 @@ ${details}
}

const scores: any = assignment
.getIn(["nbgrader_scores", student_id], Map())
.getIn(["nbgrader_scores", student_id], iMap())
.toJS();
let x: any = scores[filename];
if (x == null) {
@@ -1896,7 +1891,7 @@ ${details}
]);

const course_project_id = store.get("course_project_id");
const student_project_id = student.get("project_id");
const student_project_id = this.getProjectId({ assignment, student });

let grade_project_id: string;
let student_path: string;
@@ -2201,7 +2196,7 @@ ${details}
const store = this.get_store();
let nbgrader_run_info: NBgraderRunInfo = store.get(
"nbgrader_run_info",
Map(),
iMap(),
);
const key = student_id ? `${assignment_id}-${student_id}` : assignment_id;
nbgrader_run_info = nbgrader_run_info.set(key, webapp_client.server_time());
@@ -2215,7 +2210,7 @@ ${details}
const store = this.get_store();
let nbgrader_run_info: NBgraderRunInfo = store.get(
"nbgrader_run_info",
Map<string, number>(),
iMap<string, number>(),
);
const key = student_id ? `${assignment_id}-${student_id}` : assignment_id;
nbgrader_run_info = nbgrader_run_info.delete(key);
@@ -2297,4 +2292,71 @@ ${details}
set_activity({ id });
}
};

setLocation = (assignment_id: string, location: AssignmentLocation) => {
this.course_actions.set({ table: "assignments", assignment_id, location });
};

getProjectId = ({
assignment,
student,
}: {
assignment;
student;
}): string | null | undefined => {
const location = getLocation(assignment);
if (location == "group") {
const group = assignment.getIn(["groups", student.get("student_id")]);
if (group != null) {
return assignment.getIn(["group_projects", group]);
}
return null;
} else if (location == "exam") {
return assignment.getIn(["exam_projects", student.get("student_id")]);
} else {
return student.get("project_id");
}
};

private getOrCreateProjectId = async ({
assignment,
student,
}: {
assignment;
student;
create?: boolean;
}): Promise<string> => {
let student_project_id = this.getProjectId({ assignment, student });
if (student_project_id != null) {
return student_project_id;
}
const location = getLocation(assignment);
const student_id = student.get("student_id");
const assignment_id = assignment.get("assignment_id");
let project_id;
if (location == "individual") {
project_id =
await this.course_actions.student_projects.create_student_project(
student_id,
);
} else if (location == "exam") {
project_id =
await this.course_actions.student_projects.createProjectForStudentUse({
student_id,
type: "exam",
});
const exam_projects = assignment.get("exam_projects") ?? iMap({});
this.set_assignment_field(
assignment_id,
"exam_projects",
exam_projects.set(student_id, project_id),
);
} else if (location == "group") {
throw Error("create group project: not implemented");
}
if (!project_id) {
throw Error("failed to create project");
}
return project_id;
};
}
19 changes: 11 additions & 8 deletions src/packages/frontend/course/assignments/assignment.tsx
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ import { capitalize, trunc_middle } from "@cocalc/util/misc";
import { Alert, Button, Card, Col, Input, Popconfirm, Row, Space } from "antd";
import { ReactElement, useState } from "react";
import { DebounceInput } from "react-debounce-input";
import { CourseActions } from "../actions";
import type { CourseActions } from "../actions";
import { BigTime, Progress } from "../common";
import { NbgraderButton } from "../nbgrader/nbgrader-button";
import {
@@ -39,6 +39,7 @@ import { STUDENT_SUBDIR } from "./consts";
import { StudentListForAssignment } from "./assignment-student-list";
import { ConfigurePeerGrading } from "./configure-peer";
import { SkipCopy } from "./skip";
import Location from "./location";

interface AssignmentProps {
active_feedback_edits: IsGradingMap;
@@ -268,7 +269,12 @@ export function Assignment({
};
v.push(
<Row key="header3" style={{ ...bottom, marginTop: "15px" }}>
<Col md={4}>{render_open_button()}</Col>
<Col md={4}>
<Space wrap>
{render_open_button()}
<Location assignment={assignment} actions={actions} />
</Space>
</Col>
<Col md={20}>
<Row>
<Col md={12} style={{ fontSize: "14px" }} key="due">
@@ -431,10 +437,10 @@ export function Assignment({
<Icon name="folder-open" /> Open Folder
</span>
}
tip="Open the directory in the current project that contains the original files for this assignment. Edit files in this folder to create the content that your students will see when they receive an assignment."
tip="Open the folder in the current project that contains the original files for this assignment. Edit files in this folder to create the content that your students will see when they receive an assignment."
>
<Button onClick={open_assignment_path}>
<Icon name="folder-open" /> Open...
<Icon name="folder-open" /> Open
</Button>
</Tip>
);
@@ -451,10 +457,7 @@ export function Assignment({
const last_assignment = assignment.get("last_assignment");
// Primary if it hasn't been assigned before or if it hasn't started assigning.
let type;
if (
!last_assignment ||
!(last_assignment.get("time") || last_assignment.get("start"))
) {
if (!last_assignment) {
type = "primary";
} else {
type = "default";
Loading