diff --git a/docs/STYLE.md b/docs/STYLE.md index fac6846437..a95d92e61d 100644 --- a/docs/STYLE.md +++ b/docs/STYLE.md @@ -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 = (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. - diff --git a/src/packages/frontend/course/assignments/actions.ts b/src/packages/frontend/course/assignments/actions.ts index c28dbe77f2..d92bceb148 100644 --- a/src/packages/frontend/course/assignments/actions.ts +++ b/src/packages/frontend/course/assignments/actions.ts @@ -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(), + iMap(), ); 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 => { + 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; + }; } diff --git a/src/packages/frontend/course/assignments/assignment.tsx b/src/packages/frontend/course/assignments/assignment.tsx index f581fcff70..79bf12d730 100644 --- a/src/packages/frontend/course/assignments/assignment.tsx +++ b/src/packages/frontend/course/assignments/assignment.tsx @@ -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( - {render_open_button()} + + + {render_open_button()} + + + @@ -431,10 +437,10 @@ export function Assignment({ Open Folder } - 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." > ); @@ -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"; diff --git a/src/packages/frontend/course/assignments/location.tsx b/src/packages/frontend/course/assignments/location.tsx new file mode 100644 index 0000000000..8caeeb3a98 --- /dev/null +++ b/src/packages/frontend/course/assignments/location.tsx @@ -0,0 +1,183 @@ +/* +Configure the location of this assignment. + +The location is one of these: + +- 'individual': Student's personal project (the default) +- 'exam': Student's exam project \- they only have access **during the exam** +- 'group': Group project \- need nice ui to divide students into groups and let instructor customize + +The location can't be changed once any assignments have been assigned. + +This component is responsible for: + +- Displaying the selected location +- Changing the location +- Editing the groups in case of 'group' +*/ + +import { useState } from "react"; +import type { AssignmentLocation, AssignmentRecord } from "../store"; +import type { CourseActions } from "../actions"; +import { Alert, Button, Divider, Modal, Radio, Tooltip } from "antd"; +import type { CheckboxOptionType } from "antd"; +import { Icon } from "@cocalc/frontend/components/icon"; + +const LOCATIONS = { + individual: { + color: "#006ab5", + icon: "user", + label: "Individual", + desc: "their own personal course project", + }, + exam: { + color: "darkgreen", + icon: "graduation-cap", + label: "Exam", + desc: "an exam-specific project that they have access to only during the exam", + }, + group: { + color: "#8b0000", + icon: "users", + label: "Group", + desc: "an assignment-specific project with a configurable group of other students", + }, +}; + +export default function Location({ + assignment, + actions, +}: { + assignment: AssignmentRecord; + actions: CourseActions; +}) { + const [open, setOpen] = useState(false); + const location = getLocation(assignment); + const { icon, label, desc, color } = LOCATIONS[location] ?? { + label: "Bug", + icon: "bug", + }; + return ( + <> + {open && ( + + )} + + Students work on their copy of '{assignment.get("path")}' in {desc}. + + } + > + + + + ); +} + +function EditLocation({ assignment, actions, setOpen }) { + const last_assignment = assignment.get("last_assignment"); + let disabled = false; + if (last_assignment != null) { + const store = actions.get_store(); + const status = store?.get_assignment_status( + assignment.get("assignment_id"), + ); + if ((status?.assignment ?? 0) > 0) { + disabled = true; + } + } + const options: CheckboxOptionType[] = []; + const curLocation = getLocation(assignment); + for (const location in LOCATIONS) { + const { icon, label, desc, color } = LOCATIONS[location]; + options.push({ + label: ( +
+ + {label}{" "} + {" "} + - students work in {desc} +
+ ), + value: location, + disabled: disabled && location != curLocation, + }); + } + return ( + + Location Where Students Work on ' + {assignment.get("path")}' + + } + onCancel={() => setOpen(false)} + onOk={() => setOpen(false)} + cancelButtonProps={{ style: { display: "none" } }} + okText="Close" + > + { + actions.assignments.setLocation( + assignment.get("assignment_id"), + e.target.value, + ); + }} + /> + {disabled && ( + + )} + {curLocation == "group" && ( + + )} + + ); +} + +export function getLocation(assignment): AssignmentLocation { + const location = assignment.get("location") ?? "individual"; + if (location == "individual" || location == "exam" || location == "group") { + return location; + } + return "individual"; +} + +function getGroups(assignment) { + const groups = assignment.get("groups")?.toJS(); + if (groups == null || typeof groups != "object") { + return {}; + } + return groups; +} + +function GroupConfiguration({ assignment, actions, disabled }) { + const groups = getGroups(assignment); + console.log({ assignment, actions, disabled }); + return ( +
+ Group Configuration + TODO: Group configuration for assignment: {JSON.stringify(groups)} +
+ ); +} diff --git a/src/packages/frontend/course/common/student-assignment-info-header.tsx b/src/packages/frontend/course/common/student-assignment-info-header.tsx index bedec5f8c1..b735fb9416 100644 --- a/src/packages/frontend/course/common/student-assignment-info-header.tsx +++ b/src/packages/frontend/course/common/student-assignment-info-header.tsx @@ -7,6 +7,7 @@ import { Tip } from "@cocalc/frontend/components"; import { unreachable } from "@cocalc/util/misc"; import { Col, Row } from "antd"; import { AssignmentCopyStep } from "../types"; +import { useFrameContext } from "@cocalc/frontend/frame-editors/frame-tree/frame-context"; interface StudentAssignmentInfoHeaderProps { title: string; @@ -17,42 +18,49 @@ export function StudentAssignmentInfoHeader({ title, peer_grade, }: StudentAssignmentInfoHeaderProps) { - function tip_title(key: AssignmentCopyStep | "grade"): { - tip: string; - title: string; - } { + const { actions } = useFrameContext(); + function tip_title(key: AssignmentCopyStep | "grade") { switch (key) { case "assignment": return { title: "Assign to Student", - tip: "This column gives the status of making homework available to students, and lets you copy homework to one student at a time.", + tip: "Status of making assignment available to students; also, you can copy assignment to one student at a time.", }; case "collect": return { title: "Collect from Student", - tip: "This column gives status information about collecting homework from students, and lets you collect from one student at a time.", + tip: "Status information about collecting assignments from students; also, you can collect from one student at a time.", }; case "grade": return { - title: "Record Homework Grade", - tip: "Use this column to record the grade the student received on the assignment. Once the grade is recorded, you can return the assignment. You can also export grades to a file in the Configuration tab. Enter anything here; it does not have to be a number.", + title: "Record Assignment Grade", + tip: ( + <> + Record the grade the student received on the assignment. Once the + grade is recorded, you can return the assignment. You can also{" "} + (actions as any)?.setModal?.("export-grades")}> + export grades to a file in the Actions tab + + . Enter anything here; it does not have to be a number. + + ), }; case "peer_assignment": return { title: "Assign Peer Grading", - tip: "This column gives the status of sending out collected homework to students for peer grading.", + tip: "Status of sending out collected assignment to students for peer grading.", }; case "peer_collect": return { title: "Collect Peer Grading", - tip: "This column gives status information about collecting the peer grading work that students did, and lets you collect peer grading from one student at a time.", + tip: "Status information about collecting the peer grading work that students did; also, you can collect peer grading from one student at a time.", }; case "return_graded": return { title: "Return to Student", - tip: "This column gives status information about when you returned homework to the students. Once you have entered a grade, you can return the assignment.", + tip: "Status information about when you returned assignment to the students. Once you have entered a grade, you can return the assignment.", }; default: unreachable(key); diff --git a/src/packages/frontend/course/store.ts b/src/packages/frontend/course/store.ts index 618532c20d..3578bdd37f 100644 --- a/src/packages/frontend/course/store.ts +++ b/src/packages/frontend/course/store.ts @@ -82,6 +82,8 @@ export type LastCopyInfo = { start?: number; }; +export type AssignmentLocation = "individual" | "exam" | "group"; + export type AssignmentRecord = TypedMap<{ assignment_id: string; deleted: boolean; @@ -92,6 +94,16 @@ export type AssignmentRecord = TypedMap<{ due_date: number; map: { [student_id: string]: string[] }; // map from student_id to *who* will grade that student }; + + location?: AssignmentLocation; + groups?: { + // Map student to the group they are in for this group assignment. + // This is only used when AssignmentLocation is 'group'. + [student_id: string]: string; + }; + group_projects?: { [group: string]: string }; + exam_projects?: { [student_id: string]: string }; + note: string; last_assignment?: { [student_id: string]: LastCopyInfo }; diff --git a/src/packages/frontend/course/student-projects/actions.ts b/src/packages/frontend/course/student-projects/actions.ts index eb351f7ee4..14051d17a6 100644 --- a/src/packages/frontend/course/student-projects/actions.ts +++ b/src/packages/frontend/course/student-projects/actions.ts @@ -39,13 +39,18 @@ export class StudentProjectsActions { return store; }; - // Create and configure a single student project. - create_student_project = async ( - student_id: string, - ): Promise => { + // create project that will get used by this student, but doesn't actually + // add student as a collaborator or save the project id anywhere. + createProjectForStudentUse = async ({ + student_id, + type, + }: { + student_id: string; + type: "student" | "exam" | "group"; + }): Promise => { const { store, student } = this.course_actions.resolve({ student_id, - finish: this.course_actions.set_error.bind(this), + finish: this.course_actions.set_error, }); if (store == null || student == null) return; if (store.get("students") == null || store.get("settings") == null) { @@ -54,27 +59,33 @@ export class StudentProjectsActions { ); return; } - if (student.get("project_id")) { - // project already created. - return student.get("project_id"); - } - this.course_actions.set({ - create_project: webapp_client.server_time(), - table: "students", - student_id, - }); const id = this.course_actions.set_activity({ desc: `Create project for ${store.get_student_name(student_id)}.`, }); - const dflt_img = await redux.getStore("customize").getDefaultComputeImage(); + const defaultImage = await redux + .getStore("customize") + .getDefaultComputeImage(); let project_id: string; try { project_id = await redux.getActions("projects").create_project({ title: store.get("settings").get("title"), description: store.get("settings").get("description"), - image: store.get("settings").get("custom_image") ?? dflt_img, + image: store.get("settings").get("custom_image") ?? defaultImage, noPool: true, // student is unlikely to use the project right *now* }); + this.configure_project_visibility(project_id); + + // important to at least set the basics of the course field, since this + // modifies security model so any instructor can add colabs to this project, + // even if the instructor wasn't added -- that just deals with an edge + // case that often causes problems otherwise. + const actions = redux.getActions("projects"); + await actions.set_project_course_info({ + project_id, + course_project_id: store.get("course_project_id"), + path: store.get("course_filename"), + type, + }); } catch (err) { this.course_actions.set_error( `error creating student project for ${store.get_student_name( @@ -85,16 +96,43 @@ export class StudentProjectsActions { } finally { this.course_actions.clear_activity(id); } + return project_id; + }; + + // Create and configure a single student project. + create_student_project = async ( + student_id: string, + ): Promise => { + const { student } = this.course_actions.resolve({ + student_id, + }); + if (student == null) { + // no such student -- nothing to do + return; + } + if (student.get("project_id")) { + // project already created. + return student.get("project_id"); + } this.course_actions.set({ - create_project: null, - project_id, + create_project: webapp_client.server_time(), table: "students", student_id, }); + const project_id = await this.createProjectForStudentUse({ + student_id, + type: "student", + }); await this.configure_project({ student_id, student_project_id: project_id, }); + this.course_actions.set({ + create_project: null, + project_id, + table: "students", + student_id, + }); return project_id; }; @@ -561,15 +599,17 @@ export class StudentProjectsActions { } }; - private configure_project = async (props: { + private configure_project = async ({ + student_id, + student_project_id, + force_send_invite_by_email, + license_id, + }: { student_id; student_project_id?: string; force_send_invite_by_email?: boolean; license_id?: string; // relevant for serial license strategy only }): Promise => { - const { student_id, force_send_invite_by_email, license_id } = props; - let student_project_id = props.student_project_id; - // student_project_id is optional. Will be used instead of from student_id store if provided. // Configure project for the given student so that it has the right title, // description, and collaborators for belonging to the indicated student. @@ -605,8 +645,10 @@ export class StudentProjectsActions { ): Promise => { const store = this.get_store(); if (store == null) return; - const dflt_img = await redux.getStore("customize").getDefaultComputeImage(); - const img_id = store.get("settings").get("custom_image") ?? dflt_img; + const defaultImage = await redux + .getStore("customize") + .getDefaultComputeImage(); + const img_id = store.get("settings").get("custom_image") ?? defaultImage; const actions = redux.getProjectActions(student_project_id); await actions.set_compute_image(img_id); }; @@ -922,4 +964,32 @@ export class StudentProjectsActions { } } }; + + configureExamProject = async ({ assignment, student, mode }) => { + /* + If the project hasn't already been created, do nothing. Otherwise: + + mode = 'exam': + - set collabs on exam project to be exactly the student and all collabs on course project + - make project visible only to the student + - configure project title and description + - configure project image + - configure project license + + mode = 'instructor' + - remove the student + */ + const project_id = this.course_actions.assignments.getProjectId({ + assignment, + student, + }); + if (project_id == null) { + return; + } + if (mode == "exam") { + } else { + } + }; + + //configureGroupProject = async ({ assignment, student }) => {}; } diff --git a/src/packages/frontend/course/types.ts b/src/packages/frontend/course/types.ts index 6603ad9c2f..4f8a0f801d 100644 --- a/src/packages/frontend/course/types.ts +++ b/src/packages/frontend/course/types.ts @@ -8,7 +8,11 @@ import { NotebookScores } from "../jupyter/nbgrader/autograde"; import { Datastore, EnvVars } from "../projects/actions"; import { StudentProjectFunctionality } from "./configuration/customize-student-project-functionality"; import type { PurchaseInfo } from "@cocalc/util/licenses/purchase/types"; -import type { CopyConfigurationOptions, CopyConfigurationTargets } from "./configuration/configuration-copying"; +import type { + CopyConfigurationOptions, + CopyConfigurationTargets, +} from "./configuration/configuration-copying"; +import type { AssignmentLocation } from "./store"; export interface SyncDBRecordBase { table: string; @@ -58,6 +62,11 @@ export interface SyncDBRecordAssignment { nbgrader?: boolean; // Very likely to be using nbgrader for this assignment (heuristic: existence of foo.ipynb and student/foo.ipynb) description?: string; title?: string; + location?: AssignmentLocation; + exam_projects?: { [student_id: string]: string }; + group_projects?: { [group: string]: string }; + + groups?: { [student_id: string]: string }; grades?: { [student_id: string]: string }; comments?: { [student_id: string]: string }; nbgrader_scores?: { diff --git a/src/packages/frontend/project/history/log-entry.tsx b/src/packages/frontend/project/history/log-entry.tsx index e65e15a0b6..95d7e68ce6 100644 --- a/src/packages/frontend/project/history/log-entry.tsx +++ b/src/packages/frontend/project/history/log-entry.tsx @@ -804,9 +804,9 @@ export const LogEntry: React.FC = React.memo( case "undelete_project": return undeleted the project; case "hide_project": - return hid the project from themself; + return hid the project from themselves; case "unhide_project": - return unhid the project from themself; + return unhid the project from themselves; case "public_path": return render_public_path(event); case "software_environment": diff --git a/src/packages/frontend/projects/actions.ts b/src/packages/frontend/projects/actions.ts index 1618a2b0da..2c263dd799 100644 --- a/src/packages/frontend/projects/actions.ts +++ b/src/packages/frontend/projects/actions.ts @@ -290,11 +290,11 @@ export class ProjectsActions extends Actions { } } - public async set_project_course_info({ + set_project_course_info = async ({ project_id, course_project_id, path, - pay, + pay = "", payInfo, account_id, email_address, @@ -306,15 +306,15 @@ export class ProjectsActions extends Actions { project_id: string; course_project_id: string; path: string; - pay: Date | string; + pay?: Date | string; payInfo?: PurchaseInfo | null; account_id?: string | null; email_address?: string | null; - datastore: Datastore; - type: "student" | "shared" | "nbgrader"; + datastore?: Datastore; + type: "student" | "shared" | "nbgrader" | "exam" | "group"; student_project_functionality?: StudentProjectFunctionality | null; envvars?: EnvVars; - }): Promise { + }): Promise => { if (!(await this.have_project(project_id))) { const msg = `Can't set course info -- you are not a collaborator on project '${project_id}'.`; console.warn(msg); @@ -350,7 +350,7 @@ export class ProjectsActions extends Actions { return; } return await api("projects/course/set-course-info", { project_id, course }); - } + }; // Create a new project; returns the project_id of the new project. public async create_project(opts: { diff --git a/src/packages/util/db-schema/projects.ts b/src/packages/util/db-schema/projects.ts index 992998e5f8..9618d33735 100644 --- a/src/packages/util/db-schema/projects.ts +++ b/src/packages/util/db-schema/projects.ts @@ -642,7 +642,7 @@ export interface StudentProjectFunctionality { } export interface CourseInfo { - type: "student" | "shared" | "nbgrader"; + type: "student" | "shared" | "nbgrader" | "exam" | "group"; account_id?: string; // account_id of the student that this project is for. project_id: string; // the course project, i.e., project with the .course file path: string; // path to the .course file in project_id diff --git a/src/scripts/g b/src/scripts/g new file mode 100755 index 0000000000..ad4c91c7ec --- /dev/null +++ b/src/scripts/g @@ -0,0 +1,11 @@ +mkdir -p `pwd`/logs +export LOGS=`pwd`/logs +rm -f $LOGS/log +unset INIT_CWD +unset PGHOST +export DEBUG="cocalc:*" +#export DEBUG_CONSOLE="yes" +unset DEBUG_CONSOLE + +export COCALC_DISABLE_API_VALIDATION=yes +pnpm hub diff --git a/src/scripts/g-tmux.sh b/src/scripts/g-tmux.sh index 012ee82975..56c31e8e2c 100755 --- a/src/scripts/g-tmux.sh +++ b/src/scripts/g-tmux.sh @@ -6,4 +6,4 @@ sleep 1 tmux send-keys -t mysession:1 './scripts/g.sh' C-m sleep 1 tmux send-keys -t mysession:0 'pnpm database' C-m -tmux attach -t mysession \ No newline at end of file +tmux attach -t mysession