Skip to content
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

courses: add exams and group assignments #7888

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Up @@ -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";
Expand All @@ -47,6 +47,7 @@ import {
CourseStore,
get_nbgrader_score,
NBgraderRunInfo,
AssignmentLocation,
} from "../store";
import {
AssignmentCopyType,
Expand All @@ -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;

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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"),
Expand All @@ -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"),
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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());
Expand All @@ -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);
Expand Down Expand Up @@ -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
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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>
);
Expand All @@ -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";
Expand Down
Loading