Path: blob/master/src/packages/frontend/course/handouts/handout.tsx
1503 views
/*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";8import { CSS, redux } from "@cocalc/frontend/app-framework";9import { Icon, MarkdownInput, Tip } from "@cocalc/frontend/components";10import { course, labels } from "@cocalc/frontend/i18n";11import { UserMap } from "@cocalc/frontend/todo-types";12import { capitalize, trunc_middle } from "@cocalc/util/misc";13import type { CourseActions } from "../actions";14import { CourseStore, HandoutRecord, StudentsMap } from "../store";15import * as styles from "../styles";16import { StudentListForHandout } from "./handout-student-list";17import { ComputeServerButton } from "../compute";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 style={{ flex: 1 }} />118<ComputeServerButton unit={handout as any} actions={actions} />119<div style={{ flex: 1 }} />120{render_delete_button()}121</div>122);123}124125function render_handout_notes() {126return (127<Row key="note" style={styles.note}>128<Col xs={4}>129<Tip130title={intl.formatMessage({131id: "course.handouts.handout_notes.tooltip.title",132defaultMessage: "Notes about this handout",133})}134tip={intl.formatMessage({135id: "course.handouts.handout_notes.tooltip.tooltip",136defaultMessage: `Record notes about this handout here.137These notes are only visible to you, not to your students.138Put any instructions to students about handouts in a file in the directory139that contains the handout.`,140})}141>142<FormattedMessage143id="course.handouts.handout_notes.title"144defaultMessage={"Handout Notes"}145/>146<br />147</Tip>148</Col>149<Col xs={20}>150<MarkdownInput151persist_id={152handout.get("path") + handout.get("handout_id") + "note"153}154attach_to={name}155rows={6}156placeholder={intl.formatMessage({157id: "course.handouts.handout_notes.placeholder",158defaultMessage:159"Notes about this handout (not visible to students)",160})}161default_value={handout.get("note")}162on_save={(value) =>163actions.handouts.set_handout_note(164handout.get("handout_id"),165value,166)167}168/>169</Col>170</Row>171);172}173174function render_export_file_use_times() {175return (176<Row key="file-use-times-export-handout">177<Col xs={4}>178<Tip179title="Export when students used files"180tip="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."181>182Export file use times183<br />184</Tip>185</Col>186<Col xs={20}>187<Button188onClick={() =>189actions.export.file_use_times(handout.get("handout_id"))190}191>192Export file use times for this handout193</Button>194</Col>195</Row>196);197}198199function render_copy_all(status) {200const steps = STEPS;201const result: (React.JSX.Element | undefined)[] = [];202for (const step of steps) {203if (copy_confirm_handout) {204result.push(render_copy_confirm(step, status));205} else {206result.push(undefined);207}208}209return result;210}211212function render_copy_confirm(step: string, status) {213return (214<span key={`copy_confirm_${step}`}>215{status[step] === 0216? render_copy_confirm_to_all(step, status)217: undefined}218{status[step] !== 0219? render_copy_confirm_to_all_or_new(step, status)220: undefined}221</span>222);223}224225function render_copy_cancel() {226const cancel = (): void => {227set_copy_confirm_handout(false);228set_copy_confirm_all_handout(false);229set_copy_confirm(false);230set_copy_handout_confirm_overwrite(false);231};232return (233<Button key="cancel" onClick={cancel}>234{intl.formatMessage(labels.cancel)}235</Button>236);237}238239function render_copy_handout_confirm_overwrite(step: string) {240if (!copy_handout_confirm_overwrite) {241return;242}243const do_it = (): void => {244copy_handout(step, false, true);245set_copy_handout_confirm_overwrite(false);246set_copy_handout_confirm_overwrite_text("");247};248return (249<div style={{ marginTop: "15px" }}>250Type in "OVERWRITE" if you are certain to replace the handout files of251all students.252<Input253autoFocus254onChange={(e) =>255set_copy_handout_confirm_overwrite_text(e.target.value)256}257style={{ marginTop: "1ex" }}258/>259<Space style={{ textAlign: "center", marginTop: "15px" }}>260{render_copy_cancel()}261<Button262disabled={copy_handout_confirm_overwrite_text !== "OVERWRITE"}263danger264onClick={do_it}265>266<Icon name="exclamation-triangle" /> Confirm replacing files267</Button>268</Space>269</div>270);271}272273function copy_handout(step, new_only, overwrite?): void {274// handout to all (non-deleted) students275switch (step) {276case "handout":277actions.handouts.copy_handout_to_all_students(278handout.get("handout_id"),279new_only,280overwrite,281);282break;283default:284console.log(`BUG -- unknown step: ${step}`);285}286set_copy_confirm_handout(false);287set_copy_confirm_all_handout(false);288set_copy_confirm(false);289}290291function render_copy_confirm_to_all(step, status) {292const n = status[`not_${step}`];293return (294<Alert295type="warning"296key={`${step}_confirm_to_all`}297style={{ marginTop: "15px" }}298message={299<div>300<div style={{ marginBottom: "15px" }}>301{capitalize(step_verb(step))} this handout {step_direction(step)}{" "}302the {n} student{n > 1 ? "s" : ""}303{step_ready(step)}?304</div>305<Space>306{render_copy_cancel()}307<Button308key="yes"309type="primary"310onClick={() => copy_handout(step, false)}311>312Yes313</Button>314</Space>315</div>316}317/>318);319}320321function copy_confirm_all_caution(step): string | undefined {322switch (step) {323case "handout":324return `\325This will recopy all of the files to them.326CAUTION: 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.327Select "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.\328`;329}330}331332function render_copy_confirm_overwrite_all(step) {333return (334<div key="copy_confirm_overwrite_all" style={{ marginTop: "15px" }}>335<div style={{ marginBottom: "15px" }}>336{copy_confirm_all_caution(step)}337</div>338<Space wrap>339{render_copy_cancel()}340<Button key="all" onClick={() => copy_handout(step, false)}>341Yes, do it342</Button>343<Button344key="all-overwrite"345danger346onClick={() => set_copy_handout_confirm_overwrite(true)}347>348Replace student files!349</Button>350</Space>351{render_copy_handout_confirm_overwrite(step)}352</div>353);354}355356function render_copy_confirm_to_all_or_new(step, status) {357const n = status[`not_${step}`];358const m = n + status[step];359return (360<Alert361type="warning"362key={`${step}_confirm_to_all_or_new`}363style={{ marginTop: "15px" }}364message={365<div>366<div style={{ marginBottom: "15px" }}>367{capitalize(step_verb(step))} this handout {step_direction(step)}368...369</div>370<Space wrap>371{render_copy_cancel()}372<Button373key="all"374danger375onClick={() => {376set_copy_confirm_all_handout(true);377set_copy_confirm(true);378}}379disabled={copy_confirm_all_handout}380>381{step === "handout" ? "All" : "The"} {m} students382{step_ready(step)}383...384</Button>385{n ? (386<Button387key="new"388type="primary"389onClick={() => copy_handout(step, true)}390>391The {n} student{n > 1 ? "s" : ""} not already{" "}392{past_tense(step_verb(step))} {step_direction(step)}393</Button>394) : undefined}395</Space>396{copy_confirm_all_handout397? render_copy_confirm_overwrite_all(step)398: undefined}399</div>400}401/>402);403}404405function render_handout_button(status) {406const handout_count = status.handout;407const { not_handout } = status;408let type;409if (handout_count === 0) {410type = "primary";411} else {412if (not_handout === 0) {413type = "dashed";414} else {415type = "default";416}417}418const tooltip = intl.formatMessage({419id: "course.handouts.handout_button.tooltip",420defaultMessage:421"Copy the files for this handout from this project to all other student projects.",422description: "student in an online course",423});424const label = intl.formatMessage(course.handout);425const you = intl.formatMessage(labels.you);426const students = intl.formatMessage(course.students);427428return (429<Button430key="handout"431type={type}432onClick={() => {433set_copy_confirm_handout(true);434set_copy_confirm(true);435}}436disabled={copy_confirm}437style={outside_button_style}438>439<Tip440title={441<span>442{label}: <Icon name="user-secret" /> {you}{" "}443<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}444</span>445}446tip={tooltip}447>448<Icon name="share-square" /> {intl.formatMessage(course.distribute)}449...450</Tip>451</Button>452);453}454455function delete_handout(): void {456actions.handouts.delete_handout(handout.get("handout_id"));457}458459function undelete_handout(): void {460actions.handouts.undelete_handout(handout.get("handout_id"));461}462463function render_delete_button() {464if (handout.get("deleted")) {465return (466<Tip467key="delete"468placement="left"469title="Undelete handout"470tip="Make the handout visible again in the handout list and in student grade lists."471>472<Button onClick={undelete_handout}>473<Icon name="trash" /> Undelete474</Button>475</Tip>476);477} else {478return (479<Popconfirm480key="delete"481onConfirm={delete_handout}482title={483<div style={{ maxWidth: "400px" }}>484<b>485Are you sure you want to delete "486{trunc_middle(handout.get("path"), 24)}"?487</b>488<br />489This removes it from the handout list and student grade lists, but490does not delete any files off of disk. You can always undelete an491handout later by showing it using the 'show deleted handouts'492button.493</div>494}495>496<Button>497<Icon name="trash" /> Delete...498</Button>499</Popconfirm>500);501}502}503504function render_more() {505if (!is_expanded) return;506return (507<Row key="more">508<Col sm={24}>509<Card title={render_more_header()}>510<StudentListForHandout511frame_id={frame_id}512handout={handout}513students={students}514user_map={user_map}515actions={actions}516name={name}517/>518{render_handout_notes()}519<br />520<hr />521<br />522{render_export_file_use_times()}523</Card>524</Col>525</Row>526);527}528529const outside_button_style: CSS = {530margin: "4px",531paddingTop: "6px",532paddingBottom: "4px",533};534535function render_handout_name() {536return (537<h5>538<a539href=""540onClick={(e) => {541e.preventDefault();542return actions.toggle_item_expansion(543"handout",544handout.get("handout_id"),545);546}}547>548<Icon549style={{ marginRight: "10px", float: "left" }}550name={is_expanded ? "caret-down" : "caret-right"}551/>552<div>553{trunc_middle(handout.get("path"), 24)}554{handout.get("deleted") ? <b> (deleted)</b> : undefined}555</div>556</a>557</h5>558);559}560561function get_store(): CourseStore {562const store = redux.getStore(name);563if (store == null) throw Error("store must be defined");564return store as unknown as CourseStore;565}566567function render_handout_heading() {568let status = get_store().get_handout_status(handout.get("handout_id"));569if (status == null) {570status = {571handout: 0,572not_handout: 0,573};574}575return (576<Row key="summary" style={{ backgroundColor: backgroundColor }}>577<Col md={8} style={{ paddingRight: "0px" }}>578{render_handout_name()}579</Col>580<Col md={16}>581<Row style={{ marginLeft: "8px" }}>582{render_handout_button(status)}583<span584style={{ color: "#666", marginLeft: "5px", marginTop: "10px" }}585>586({status.handout}/{status.handout + status.not_handout}{" "}587transferred)588</span>589</Row>590<Row style={{ marginLeft: "8px" }}>{render_copy_all(status)}</Row>591</Col>592</Row>593);594}595596return (597<div>598<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>599<Col xs={24} style={{ paddingTop: "5px", paddingBottom: "5px" }}>600{render_handout_heading()}601{render_more()}602</Col>603</Row>604</div>605);606}607608609