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/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// CoCalc libraries6import { SyncDB } from "@cocalc/sync/editor/db/sync";7import { SyncDBRecord } from "./types";8// Course Library9import {10CourseState,11CourseStore,12AssignmentRecord,13StudentRecord,14HandoutRecord,15} from "./store";16import { SharedProjectActions } from "./shared-project/actions";17import { ActivityActions } from "./activity/actions";18import { StudentsActions } from "./students/actions";19import { StudentProjectsActions } from "./student-projects/actions";20import { AssignmentsActions } from "./assignments/actions";21import { HandoutsActions } from "./handouts/actions";22import { ConfigurationActions } from "./configuration/actions";23import { ExportActions } from "./export/actions";24import { ProjectsStore } from "../projects/store";25import { bind_methods } from "@cocalc/util/misc";26// React libraries27import { Actions, TypedMap } from "../app-framework";28import { Map as iMap } from "immutable";2930export const primary_key = {31students: "student_id",32assignments: "assignment_id",33handouts: "handout_id",34};3536// Requires a syncdb to be set later37// Manages local and sync changes38export class CourseActions extends Actions<CourseState> {39public syncdb: SyncDB;40private last_collaborator_state: any;41private activity: ActivityActions;42public students: StudentsActions;43public student_projects: StudentProjectsActions;44public shared_project: SharedProjectActions;45public assignments: AssignmentsActions;46public handouts: HandoutsActions;47public configuration: ConfigurationActions;48public export: ExportActions;49private state: "init" | "ready" | "closed" = "init";5051constructor(name, redux) {52super(name, redux);53if (this.name == null || this.redux == null) {54throw Error("BUG: name and redux must be defined");55}5657this.shared_project = bind_methods(new SharedProjectActions(this));58this.activity = bind_methods(new ActivityActions(this));59this.students = bind_methods(new StudentsActions(this));60this.student_projects = bind_methods(new StudentProjectsActions(this));61this.assignments = bind_methods(new AssignmentsActions(this));62this.handouts = bind_methods(new HandoutsActions(this));63this.configuration = bind_methods(new ConfigurationActions(this));64this.export = bind_methods(new ExportActions(this));65}6667get_store = (): CourseStore => {68const store = this.redux.getStore<CourseState, CourseStore>(this.name);69if (store == null) throw Error("store is null");70if (!this.store_is_initialized())71throw Error("course store must be initialized");72this.state = "ready"; // this is pretty dumb for now.73return store;74};7576is_closed = (): boolean => {77if (this.state == "closed") return true;78const store = this.redux.getStore<CourseState, CourseStore>(this.name);79if (store == null) {80this.state = "closed";81return true;82}83return false;84};8586private is_loaded = (): boolean => {87if (this.syncdb == null) {88this.set_error("attempt to set syncdb before loading");89return false;90}91return true;92};9394private store_is_initialized = (): boolean => {95const store = this.redux.getStore<CourseState, CourseStore>(this.name);96if (store == null) {97return false;98}99if (100!(101store.get("students") != null &&102store.get("assignments") != null &&103store.get("settings") != null &&104store.get("handouts") != null105)106) {107return false;108}109return true;110};111112// Set one object in the syncdb113set = (obj: SyncDBRecord, commit: boolean = true): void => {114if (115!this.is_loaded() ||116(this.syncdb != null ? this.syncdb.get_state() === "closed" : undefined)117) {118return;119}120// put in similar checks for other tables?121if (obj.table == "students" && obj.student_id == null) {122console.warn("course: setting student without primary key", obj);123}124this.syncdb.set(obj);125if (commit) {126this.syncdb.commit();127}128};129130delete = (obj: SyncDBRecord, commit: boolean = true): void => {131if (132!this.is_loaded() ||133(this.syncdb != null ? this.syncdb.get_state() === "closed" : undefined)134) {135return;136}137// put in similar checks for other tables?138if (obj.table == "students" && obj.student_id == null) {139console.warn("course: deleting student without primary key", obj);140}141this.syncdb.delete(obj);142if (commit) {143this.syncdb.commit();144}145};146147// Get one object from this.syncdb as a Javascript object (or undefined)148get_one = (obj: SyncDBRecord): SyncDBRecord | undefined => {149if (150this.syncdb != null ? this.syncdb.get_state() === "closed" : undefined151) {152return;153}154const x: any = this.syncdb.get_one(obj);155if (x == null) return;156return x.toJS();157};158159save = async (): Promise<void> => {160const store = this.get_store();161if (store == null) {162return;163} // e.g., if the course store object already gone due to closing course.164if (store.get("saving")) {165return; // already saving166}167const id = this.set_activity({ desc: "Saving..." });168this.setState({ saving: true });169try {170await this.syncdb.save_to_disk();171this.setState({ show_save_button: false });172} catch (err) {173this.set_error(`Error saving -- ${err}`);174this.setState({ show_save_button: true });175return;176} finally {177this.clear_activity(id);178this.setState({ saving: false });179this.update_unsaved_changes();180setTimeout(this.update_unsaved_changes.bind(this), 1000);181}182};183184syncdb_change = (changes: TypedMap<SyncDBRecord>[]): void => {185let t;186const store = this.get_store();187if (store == null) {188return;189}190const cur = (t = store.getState());191changes.map((obj) => {192const table = obj.get("table");193if (table == null) {194// no idea what to do with something that doesn't have table defined195return;196}197const x = this.syncdb.get_one(obj);198const key = primary_key[table];199if (x == null) {200// delete201if (key != null) {202t = t.set(table, t.get(table).delete(obj.get(key)));203}204} else {205// edit or insert206if (key != null) {207t = t.set(table, t.get(table).set(x.get(key), x));208} else if (table === "settings") {209t = t.set(table, t.get(table).merge(x.delete("table")));210} else {211// no idea what to do with this212console.warn(`unknown table '${table}'`);213}214}215}); // ensure map doesn't terminate216217if (!cur.equals(t)) {218// something definitely changed219this.setState(t);220}221this.update_unsaved_changes();222};223224private update_unsaved_changes = (): void => {225if (this.syncdb == null) {226return;227}228const unsaved = this.syncdb.has_unsaved_changes();229this.setState({ unsaved });230};231232// important that this be bound...233handle_projects_store_update = (projects_store: ProjectsStore): void => {234const store = this.redux.getStore<CourseState, CourseStore>(this.name);235if (store == null) return; // not needed yet.236let users = projects_store.getIn([237"project_map",238store.get("course_project_id"),239"users",240]);241if (users == null) return;242users = users.keySeq();243if (this.last_collaborator_state == null) {244this.last_collaborator_state = users;245return;246}247if (!this.last_collaborator_state.equals(users)) {248this.student_projects.configure_all_projects();249}250this.last_collaborator_state = users;251};252253// Set the error. Use error="" to explicitly clear the existing set error.254// If there is an error already set, then the new error is just255// appended to the existing one.256set_error = (error: string): void => {257if (error != "") {258const store = this.get_store();259if (store == null) return;260if (store.get("error")) {261error = `${store.get("error")} \n${error}`;262}263error = error.trim();264}265this.setState({ error });266};267268// ACTIVITY ACTIONS269set_activity = (270opts: { id: number; desc?: string } | { id?: number; desc: string },271): number => {272return this.activity.set_activity(opts);273};274275clear_activity = (id?: number): void => {276this.activity.clear_activity(id);277};278279// CONFIGURATION ACTIONS280// These hang off of this.configuration281282// SHARED PROJECT ACTIONS283// These hang off of this.shared_project284285// STUDENTS ACTIONS286// These hang off of this.students287288// STUDENT PROJECTS ACTIONS289// These all hang off of this.student_projects now.290291// ASSIGNMENT ACTIONS292// These all hang off of this.assignments now.293294// HANDOUT ACTIONS295// These all hang off of this.handouts now.296297// UTILITY FUNCTIONS298299/* Utility function that makes getting student/assignment/handout300object associated to an id cleaner, since we do this a LOT in301our code, and there was a lot of code duplication as a result.302If something goes wrong and the finish function is defined, then303it is called with a string describing the error.304*/305resolve = (opts: {306assignment_id?: string;307student_id?: string;308handout_id?: string;309finish?: Function;310}) => {311const r: {312student?: StudentRecord;313assignment?: AssignmentRecord;314handout?: HandoutRecord;315store: CourseStore;316} = { store: this.get_store() };317318if (opts.student_id) {319const student = this.syncdb?.get_one({320table: "students",321student_id: opts.student_id,322}) as StudentRecord | undefined;323if (student == null) {324if (opts.finish != null) {325console.trace();326opts.finish("no student " + opts.student_id);327return r;328}329} else {330r.student = student;331}332}333if (opts.assignment_id) {334const assignment = this.syncdb?.get_one({335table: "assignments",336assignment_id: opts.assignment_id,337}) as AssignmentRecord | undefined;338if (assignment == null) {339if (opts.finish != null) {340opts.finish("no assignment " + opts.assignment_id);341return r;342}343} else {344r.assignment = assignment;345}346}347if (opts.handout_id) {348const handout = this.syncdb?.get_one({349table: "handouts",350handout_id: opts.handout_id,351}) as HandoutRecord | undefined;352if (handout == null) {353if (opts.finish != null) {354opts.finish("no handout " + opts.handout_id);355return r;356}357} else {358r.handout = handout;359}360}361return r;362};363364// Takes an item_name and the id of the time365// item_name should be one of366// ['student', 'assignment', 'peer_config', handout', 'skip_grading']367toggle_item_expansion = (368item_name:369| "student"370| "assignment"371| "peer_config"372| "handout"373| "skip_grading",374item_id,375): void => {376let adjusted;377const store = this.get_store();378if (store == null) {379return;380}381const field_name: any = `expanded_${item_name}s`;382const expanded_items = store.get(field_name);383if (expanded_items.has(item_id)) {384adjusted = expanded_items.delete(item_id);385} else {386adjusted = expanded_items.add(item_id);387if (item_name == "assignment") {388// for assignments, whenever show more details also update the directory listing,389// since various things that get rendered in the expanded view depend on an updated listing.390this.assignments.update_listing(item_id);391}392}393this.setState({ [field_name]: adjusted });394};395396setPageFilter = (page: string, filter: string) => {397const store = this.get_store();398if (!store) return;399let pageFilter = store.get("pageFilter");400if (pageFilter == null) {401if (filter) {402pageFilter = iMap({ [page]: filter });403this.setState({404pageFilter,405});406}407return;408}409pageFilter = pageFilter.set(page, filter);410this.setState({ pageFilter });411};412}413414415