Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/course/handouts/actions.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Actions involving working with handouts.7*/89import type { CourseActions } from "../actions";10import type { CourseStore, HandoutRecord } from "../store";11import { webapp_client } from "../../webapp-client";12import { redux } from "../../app-framework";13import { uuid } from "@cocalc/util/misc";14import { map } from "awaiting";15import type { SyncDBRecordHandout } from "../types";16import { exec } from "../../frame-editors/generic/client";17import { export_student_file_use_times } from "../export/file-use-times";18import { COPY_TIMEOUT_MS } from "../consts";1920export class HandoutsActions {21private course_actions: CourseActions;2223constructor(course_actions: CourseActions) {24this.course_actions = course_actions;25}2627private get_store = (): CourseStore => {28return this.course_actions.get_store();29};3031// slight warning -- this is linear in the number of assignments (so do not overuse)32private getHandoutWithPath = (path: string): HandoutRecord | undefined => {33const store = this.get_store();34if (store == null) return;35return store36.get("handouts")37.valueSeq()38.filter((x) => x.get("path") == path)39.get(0);40};4142addHandout = async (path: string | string[]): Promise<void> => {43// Add one or more handouts to the course, which is defined by giving a directory in the project.44// If the handout was previously deleted, this undeletes it.45if (typeof path != "string") {46// handle case of array of inputs47for (const p of path) {48await this.addHandout(p);49}50return;51}52const cur = this.getHandoutWithPath(path);53if (cur != null) {54// either undelete or nothing to do.55if (cur.get("deleted")) {56// undelete57this.undelete_handout(cur.get("handout_id"));58} else {59// nothing to do60}61return;62}6364const target_path = path; // folder where we copy the handout to65try {66// Ensure the path actually exists in the instructor project.67await exec({68project_id: this.get_store().get("course_project_id"),69command: "mkdir",70args: ["-p", path],71err_on_exit: true,72});73} catch (err) {74this.course_actions.set_error(`error creating assignment: ${err}`);75return;76}7778this.course_actions.set({79path,80target_path,81table: "handouts",82handout_id: uuid(),83});84};8586delete_handout = (handout_id: string): void => {87this.course_actions.set({88deleted: true,89handout_id,90table: "handouts",91});92};9394undelete_handout = (handout_id: string): void => {95this.course_actions.set({96deleted: false,97handout_id,98table: "handouts",99});100};101102private set_handout_field = (handout, name, val): void => {103handout = this.get_store().get_handout(handout);104this.course_actions.set({105[name]: val,106table: "handouts",107handout_id: handout.get("handout_id"),108});109};110111set_handout_note = (handout, note): void => {112this.set_handout_field(handout, "note", note);113};114115private handout_finish_copy = (116handout_id: string,117student_id: string,118err: string,119): void => {120const { student, handout } = this.course_actions.resolve({121handout_id,122student_id,123});124if (student == null || handout == null) return;125const obj: SyncDBRecordHandout = {126table: "handouts",127handout_id: handout.get("handout_id"),128};129const h = this.course_actions.get_one(obj);130if (h == null) return;131const status_map: {132[student_id: string]: { time?: number; error?: string };133} = h.status ? h.status : {};134status_map[student_id] = { time: webapp_client.server_time() };135if (err) {136status_map[student_id].error = err;137}138obj.status = status_map;139this.course_actions.set(obj);140};141142// returns false if an actual copy starts and true if not (since we143// already tried or closed the store).144private handout_start_copy = (145handout_id: string,146student_id: string,147): boolean => {148const obj: any = {149table: "handouts",150handout_id,151};152const x = this.course_actions.get_one(obj);153if (x == null) {154// no such handout.155return true;156}157const status_map = x.status != null ? x.status : {};158let student_status = status_map[student_id];159if (student_status == null) student_status = {};160if (161student_status.start != null &&162webapp_client.server_time() - student_status.start <= 15000163) {164return true; // never retry a copy until at least 15 seconds later.165}166student_status.start = webapp_client.server_time();167status_map[student_id] = student_status;168obj.status = status_map;169this.course_actions.set(obj);170return false;171};172173// "Copy" of `stop_copying_assignment:`174stop_copying_handout = (handout_id: string, student_id: string): void => {175const obj: SyncDBRecordHandout = { table: "handouts", handout_id };176const h = this.course_actions.get_one(obj);177if (h == null) return;178const status = h.status;179if (status == null) return;180const student_status = status[student_id];181if (student_status == null) return;182if (student_status.start != null) {183delete student_status.start;184status[student_id] = student_status;185obj.status = status;186this.course_actions.set(obj);187}188};189190// Copy the files for the given handout to the given student. If191// the student project doesn't exist yet, it will be created.192// You may also pass in an id for either the handout or student.193// "overwrite" (boolean, optional): if true, the copy operation will overwrite/delete remote files in student projects -- #1483194// If the store is initialized and the student and handout both exist,195// then calling this action will result in this getting set in the store:196//197// handout.status[student_id] = {time:?, error:err}198//199// where time >= now is the current time in milliseconds.200copy_handout_to_student = async (201handout_id: string,202student_id: string,203overwrite: boolean,204): Promise<void> => {205if (this.handout_start_copy(handout_id, student_id)) {206return;207}208const id = this.course_actions.set_activity({209desc: "Copying handout to a student",210});211const finish = (err?) => {212this.course_actions.clear_activity(id);213this.handout_finish_copy(handout_id, student_id, err);214if (err) {215this.course_actions.set_error(`copy handout to student: ${err}`);216}217};218const { store, student, handout } = this.course_actions.resolve({219student_id,220handout_id,221finish,222});223if (!student || !handout) return;224225const student_name = store.get_student_name(student_id);226this.course_actions.set_activity({227id,228desc: `Copying handout to ${student_name}`,229});230let student_project_id: string | undefined = student.get("project_id");231const course_project_id = store.get("course_project_id");232const src_path = handout.get("path");233try {234if (student_project_id == null) {235this.course_actions.set_activity({236id,237desc: `${student_name}'s project doesn't exist, so creating it.`,238});239student_project_id =240await this.course_actions.student_projects.create_student_project(241student_id,242);243}244245if (student_project_id == null) {246throw Error("bug -- student project should have been created");247}248249this.course_actions.set_activity({250id,251desc: `Copying files to ${student_name}'s project`,252});253254await webapp_client.project_client.copy_path_between_projects({255src_project_id: course_project_id,256src_path,257target_project_id: student_project_id,258target_path: handout.get("target_path"),259overwrite_newer: !!overwrite, // default is "false"260delete_missing: !!overwrite, // default is "false"261backup: !!!overwrite, // default is "true"262timeout: COPY_TIMEOUT_MS / 1000,263});264finish();265} catch (err) {266finish(err);267}268};269270// Copy the given handout to all non-deleted students, doing several copies in parallel at once.271copy_handout_to_all_students = async (272handout_id: string,273new_only: boolean,274overwrite: boolean,275): Promise<void> => {276const desc: string =277"Copying handouts to all students " +278(new_only ? "who have not already received it" : "");279const short_desc = "copy handout to student";280281const id = this.course_actions.set_activity({ desc });282const finish = (err?) => {283this.course_actions.clear_activity(id);284if (err) {285err = `${short_desc}: ${err}`;286this.course_actions.set_error(err);287}288};289const { store, handout } = this.course_actions.resolve({290handout_id,291finish,292});293if (!handout) return;294295let errors = "";296const f = async (student_id: string): Promise<void> => {297if (new_only && store.handout_last_copied(handout_id, student_id)) {298return;299}300try {301await this.copy_handout_to_student(handout_id, student_id, overwrite);302} catch (err) {303errors += `\n ${err}`;304}305};306307await map(308store.get_student_ids({ deleted: false }),309store.get_copy_parallel(),310f,311);312313finish(errors);314};315316open_handout = (handout_id: string, student_id: string): void => {317const { handout, student } = this.course_actions.resolve({318handout_id,319student_id,320});321if (student == null || handout == null) return;322const student_project_id = student.get("project_id");323if (student_project_id == null) {324this.course_actions.set_error(325"open_handout: student project not yet created",326);327return;328}329const path = handout.get("target_path");330const proj = student_project_id;331if (proj == null) {332this.course_actions.set_error("no such project");333return;334}335// Now open it336redux.getProjectActions(proj).open_directory(path);337};338339export_file_use_times = async (340handout_id: string,341json_filename: string,342): Promise<void> => {343// Get the path of the handout344const { handout, store } = this.course_actions.resolve({345handout_id,346});347if (handout == null) {348throw Error("no such handout");349}350const path = handout.get("path");351await export_student_file_use_times(352store.get("course_project_id"),353path,354path,355store.get("students"),356json_filename,357store.get_student_name.bind(store),358);359};360}361362363