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/handout.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Button, Card, Col, Input, Popconfirm, Row, Space } from "antd";6import { useState } from "react";7import { FormattedMessage, useIntl } from "react-intl";89import { CSS, redux } from "@cocalc/frontend/app-framework";10import { Icon, MarkdownInput, Tip } from "@cocalc/frontend/components";11import { course, labels } from "@cocalc/frontend/i18n";12import { UserMap } from "@cocalc/frontend/todo-types";13import { capitalize, trunc_middle } from "@cocalc/util/misc";14import { CourseActions } from "../actions";15import { CourseStore, HandoutRecord, StudentsMap } from "../store";16import * as styles from "../styles";17import { StudentListForHandout } from "./handout-student-list";1819// Could be merged with steps system of assignments.20// Probably not a good idea mixing the two.21// Could also be coded into the components below but steps could be added in the future?22const STEPS = ["handout"] as const;23type STEP_TYPES = (typeof STEPS)[number];2425function step_direction(step: STEP_TYPES): string {26switch (step) {27case "handout":28return "to";29default:30throw Error(`BUG! step_direction('${step}')`);31}32}3334function step_verb(step: STEP_TYPES): string {35switch (step) {36case "handout":37return "distribute";38default:39throw Error(`BUG! step_verb('${step}')`);40}41}4243function step_ready(step: STEP_TYPES): string | undefined {44switch (step) {45case "handout":46return "";47}48}4950function past_tense(word: string): string {51if (word[word.length - 1] === "e") {52return word + "d";53} else {54return word + "ed";55}56}5758interface HandoutProps {59frame_id?: string;60name: string;61handout: HandoutRecord;62backgroundColor?: string;63actions: CourseActions;64is_expanded: boolean;65students: StudentsMap;66user_map: UserMap;67project_id: string;68}6970export function Handout({71frame_id,72name,73handout,74backgroundColor,75actions,76is_expanded,77students,78user_map,79project_id,80}: HandoutProps) {81const intl = useIntl();82const [copy_confirm, set_copy_confirm] = useState<boolean>(false);83const [copy_confirm_handout, set_copy_confirm_handout] =84useState<boolean>(false);85const [copy_confirm_all_handout, set_copy_confirm_all_handout] =86useState<boolean>(false);87const [copy_handout_confirm_overwrite, set_copy_handout_confirm_overwrite] =88useState<boolean>(false);89const [90copy_handout_confirm_overwrite_text,91set_copy_handout_confirm_overwrite_text,92] = useState<string>("");9394function open_handout_path(e) {95e.preventDefault();96const actions = redux.getProjectActions(project_id);97if (actions != null) {98actions.open_directory(handout.get("path"));99}100}101102function render_more_header() {103return (104<div style={{ display: "flex" }}>105<div106style={{107fontSize: "15pt",108marginBottom: "5px",109marginRight: "30px",110}}111>112{handout.get("path")}113</div>114<Button onClick={open_handout_path}>115<Icon name="folder-open" /> Open116</Button>117</div>118);119}120121function render_handout_notes() {122return (123<Row key="note" style={styles.note}>124<Col xs={4}>125<Tip126title={intl.formatMessage({127id: "course.handouts.handout_notes.tooltip.title",128defaultMessage: "Notes about this handout",129})}130tip={intl.formatMessage({131id: "course.handouts.handout_notes.tooltip.tooltip",132defaultMessage: `Record notes about this handout here.133These notes are only visible to you, not to your students.134Put any instructions to students about handouts in a file in the directory135that contains the handout.`,136})}137>138<FormattedMessage139id="course.handouts.handout_notes.title"140defaultMessage={"Handout Notes"}141/>142<br />143</Tip>144</Col>145<Col xs={20}>146<MarkdownInput147persist_id={148handout.get("path") + handout.get("handout_id") + "note"149}150attach_to={name}151rows={6}152placeholder={intl.formatMessage({153id: "course.handouts.handout_notes.placeholder",154defaultMessage:155"Notes about this handout (not visible to students)",156})}157default_value={handout.get("note")}158on_save={(value) =>159actions.handouts.set_handout_note(160handout.get("handout_id"),161value,162)163}164/>165</Col>166</Row>167);168}169170function render_export_file_use_times() {171return (172<Row key="file-use-times-export-handout">173<Col xs={4}>174<Tip175title="Export when students used files"176tip="Export a JSON file containing extensive information about exactly when students have opened or edited files in this handout. The JSON file will open in a new tab; the access_times (in milliseconds since the UNIX epoch) are when they opened the file and the edit_times are when they actually changed it through CoCalc's web-based editor."177>178Export file use times179<br />180</Tip>181</Col>182<Col xs={20}>183<Button184onClick={() =>185actions.export.file_use_times(handout.get("handout_id"))186}187>188Export file use times for this handout189</Button>190</Col>191</Row>192);193}194195function render_copy_all(status) {196const steps = STEPS;197const result: (JSX.Element | undefined)[] = [];198for (const step of steps) {199if (copy_confirm_handout) {200result.push(render_copy_confirm(step, status));201} else {202result.push(undefined);203}204}205return result;206}207208function render_copy_confirm(step: string, status) {209return (210<span key={`copy_confirm_${step}`}>211{status[step] === 0212? render_copy_confirm_to_all(step, status)213: undefined}214{status[step] !== 0215? render_copy_confirm_to_all_or_new(step, status)216: undefined}217</span>218);219}220221function render_copy_cancel() {222const cancel = (): void => {223set_copy_confirm_handout(false);224set_copy_confirm_all_handout(false);225set_copy_confirm(false);226set_copy_handout_confirm_overwrite(false);227};228return (229<Button key="cancel" onClick={cancel}>230{intl.formatMessage(labels.cancel)}231</Button>232);233}234235function render_copy_handout_confirm_overwrite(step: string) {236if (!copy_handout_confirm_overwrite) {237return;238}239const do_it = (): void => {240copy_handout(step, false, true);241set_copy_handout_confirm_overwrite(false);242set_copy_handout_confirm_overwrite_text("");243};244return (245<div style={{ marginTop: "15px" }}>246Type in "OVERWRITE" if you are certain to replace the handout files of247all students.248<Input249autoFocus250onChange={(e) =>251set_copy_handout_confirm_overwrite_text(e.target.value)252}253style={{ marginTop: "1ex" }}254/>255<Space style={{ textAlign: "center", marginTop: "15px" }}>256{render_copy_cancel()}257<Button258disabled={copy_handout_confirm_overwrite_text !== "OVERWRITE"}259danger260onClick={do_it}261>262<Icon name="exclamation-triangle" /> Confirm replacing files263</Button>264</Space>265</div>266);267}268269function copy_handout(step, new_only, overwrite?): void {270// handout to all (non-deleted) students271switch (step) {272case "handout":273actions.handouts.copy_handout_to_all_students(274handout.get("handout_id"),275new_only,276overwrite,277);278break;279default:280console.log(`BUG -- unknown step: ${step}`);281}282set_copy_confirm_handout(false);283set_copy_confirm_all_handout(false);284set_copy_confirm(false);285}286287function render_copy_confirm_to_all(step, status) {288const n = status[`not_${step}`];289return (290<Alert291type="warning"292key={`${step}_confirm_to_all`}293style={{ marginTop: "15px" }}294message={295<div>296<div style={{ marginBottom: "15px" }}>297{capitalize(step_verb(step))} this handout {step_direction(step)}{" "}298the {n} student{n > 1 ? "s" : ""}299{step_ready(step)}?300</div>301<Space>302{render_copy_cancel()}303<Button304key="yes"305type="primary"306onClick={() => copy_handout(step, false)}307>308Yes309</Button>310</Space>311</div>312}313/>314);315}316317function copy_confirm_all_caution(step): string | undefined {318switch (step) {319case "handout":320return `\321This will recopy all of the files to them.322CAUTION: if you update a file that a student has also worked on, their work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.323Select "Replace student files!" in case you do not want to create any backups and also delete all other files in the handout directory of their projects.\324`;325}326}327328function render_copy_confirm_overwrite_all(step) {329return (330<div key="copy_confirm_overwrite_all" style={{ marginTop: "15px" }}>331<div style={{ marginBottom: "15px" }}>332{copy_confirm_all_caution(step)}333</div>334<Space wrap>335{render_copy_cancel()}336<Button key="all" onClick={() => copy_handout(step, false)}>337Yes, do it338</Button>339<Button340key="all-overwrite"341danger342onClick={() => set_copy_handout_confirm_overwrite(true)}343>344Replace student files!345</Button>346</Space>347{render_copy_handout_confirm_overwrite(step)}348</div>349);350}351352function render_copy_confirm_to_all_or_new(step, status) {353const n = status[`not_${step}`];354const m = n + status[step];355return (356<Alert357type="warning"358key={`${step}_confirm_to_all_or_new`}359style={{ marginTop: "15px" }}360message={361<div>362<div style={{ marginBottom: "15px" }}>363{capitalize(step_verb(step))} this handout {step_direction(step)}364...365</div>366<Space wrap>367{render_copy_cancel()}368<Button369key="all"370danger371onClick={() => {372set_copy_confirm_all_handout(true);373set_copy_confirm(true);374}}375disabled={copy_confirm_all_handout}376>377{step === "handout" ? "All" : "The"} {m} students378{step_ready(step)}379...380</Button>381{n ? (382<Button383key="new"384type="primary"385onClick={() => copy_handout(step, true)}386>387The {n} student{n > 1 ? "s" : ""} not already{" "}388{past_tense(step_verb(step))} {step_direction(step)}389</Button>390) : undefined}391</Space>392{copy_confirm_all_handout393? render_copy_confirm_overwrite_all(step)394: undefined}395</div>396}397/>398);399}400401function render_handout_button(status) {402const handout_count = status.handout;403const { not_handout } = status;404let type;405if (handout_count === 0) {406type = "primary";407} else {408if (not_handout === 0) {409type = "dashed";410} else {411type = "default";412}413}414const tooltip = intl.formatMessage({415id: "course.handouts.handout_button.tooltip",416defaultMessage:417"Copy the files for this handout from this project to all other student projects.",418description: "student in an online course",419});420const label = intl.formatMessage(course.handout);421const you = intl.formatMessage(labels.you);422const students = intl.formatMessage(course.students);423424return (425<Button426key="handout"427type={type}428onClick={() => {429set_copy_confirm_handout(true);430set_copy_confirm(true);431}}432disabled={copy_confirm}433style={outside_button_style}434>435<Tip436title={437<span>438{label}: <Icon name="user-secret" /> {you}{" "}439<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}440</span>441}442tip={tooltip}443>444<Icon name="share-square" /> {intl.formatMessage(course.distribute)}445...446</Tip>447</Button>448);449}450451function delete_handout(): void {452actions.handouts.delete_handout(handout.get("handout_id"));453}454455function undelete_handout(): void {456actions.handouts.undelete_handout(handout.get("handout_id"));457}458459function render_delete_button() {460if (handout.get("deleted")) {461return (462<Tip463key="delete"464placement="left"465title="Undelete handout"466tip="Make the handout visible again in the handout list and in student grade lists."467>468<Button onClick={undelete_handout} style={outside_button_style}>469<Icon name="trash" /> Undelete470</Button>471</Tip>472);473} else {474return (475<Popconfirm476key="delete"477onConfirm={delete_handout}478title={479<div style={{ maxWidth: "400px" }}>480<b>481Are you sure you want to delete "482{trunc_middle(handout.get("path"), 24)}"?483</b>484<br />485This removes it from the handout list and student grade lists, but486does not delete any files off of disk. You can always undelete an487handout later by showing it using the 'show deleted handouts'488button.489</div>490}491>492<Button style={outside_button_style}>493<Icon name="trash" /> Delete...494</Button>495</Popconfirm>496);497}498}499500function render_more() {501if (!is_expanded) return;502return (503<Row key="more">504<Col sm={24}>505<Card title={render_more_header()}>506<StudentListForHandout507frame_id={frame_id}508handout={handout}509students={students}510user_map={user_map}511actions={actions}512name={name}513/>514{render_handout_notes()}515<br />516<hr />517<br />518{render_export_file_use_times()}519</Card>520</Col>521</Row>522);523}524525const outside_button_style: CSS = {526margin: "4px",527paddingTop: "6px",528paddingBottom: "4px",529};530531function render_handout_name() {532return (533<h5>534<a535href=""536onClick={(e) => {537e.preventDefault();538return actions.toggle_item_expansion(539"handout",540handout.get("handout_id"),541);542}}543>544<Icon545style={{ marginRight: "10px", float: "left" }}546name={is_expanded ? "caret-down" : "caret-right"}547/>548<div>549{trunc_middle(handout.get("path"), 24)}550{handout.get("deleted") ? <b> (deleted)</b> : undefined}551</div>552</a>553</h5>554);555}556557function get_store(): CourseStore {558const store = redux.getStore(name);559if (store == null) throw Error("store must be defined");560return store as unknown as CourseStore;561}562563function render_handout_heading() {564let status = get_store().get_handout_status(handout.get("handout_id"));565if (status == null) {566status = {567handout: 0,568not_handout: 0,569};570}571return (572<Row key="summary" style={{ backgroundColor: backgroundColor }}>573<Col md={8} style={{ paddingRight: "0px" }}>574{render_handout_name()}575</Col>576<Col md={8}>577<Row style={{ marginLeft: "8px" }}>578{render_handout_button(status)}579<span580style={{ color: "#666", marginLeft: "5px", marginTop: "10px" }}581>582({status.handout}/{status.handout + status.not_handout}{" "}583transferred)584</span>585</Row>586<Row style={{ marginLeft: "8px" }}>{render_copy_all(status)}</Row>587</Col>588<Col md={8}>589<Row>590<span className="pull-right">{render_delete_button()}</span>591</Row>592</Col>593</Row>594);595}596597return (598<div>599<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>600<Col xs={24} style={{ paddingTop: "5px", paddingBottom: "5px" }}>601{render_handout_heading()}602{render_more()}603</Col>604</Row>605</div>606);607}608609610