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/common/student-assignment-info.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Button, Col, Row, Space, Spin } from "antd";6import { ReactNode, useRef, useState } from "react";7import { FormattedMessage, useIntl } from "react-intl";89import { useActions } from "@cocalc/frontend/app-framework";10import { Gap, Icon, Markdown, Tip } from "@cocalc/frontend/components";11import ShowError from "@cocalc/frontend/components/error";12import { COPY_TIMEOUT_MS } from "@cocalc/frontend/course/consts";13import { MarkdownInput } from "@cocalc/frontend/editors/markdown-input";14import { labels } from "@cocalc/frontend/i18n";15import { NotebookScores } from "@cocalc/frontend/jupyter/nbgrader/autograde";16import { webapp_client } from "@cocalc/frontend/webapp-client";17import { to_json } from "@cocalc/util/misc";18import { BigTime } from ".";19import { CourseActions } from "../actions";20import { NbgraderScores } from "../nbgrader/scores";21import {22AssignmentRecord,23LastCopyInfo,24NBgraderRunInfo,25StudentRecord,26} from "../store";27import { AssignmentCopyType } from "../types";28import { useButtonSize } from "../util";29import { STEP_NAMES, Steps, STEPS_INTL, STEPS_INTL_ACTIVE } from "./consts";3031interface StudentAssignmentInfoProps {32name: string;33title: ReactNode;34student: StudentRecord;35assignment: AssignmentRecord;36grade?: string;37comments?: string;38info: {39assignment_id: string;40student_id: string;41peer_assignment: boolean;42peer_collect: boolean;43last_assignment?: LastCopyInfo;44last_collect?: LastCopyInfo;45last_peer_assignment?: LastCopyInfo;46last_peer_collect?: LastCopyInfo;47last_return_graded?: LastCopyInfo;48};49nbgrader_scores?: { [ipynb: string]: NotebookScores | string };50nbgrader_score_ids?: { [ipynb: string]: string[] };51is_editing: boolean;52nbgrader_run_info?: NBgraderRunInfo;53}5455interface RenderLastProps {56step: Steps;57type: AssignmentCopyType;58data?: any;59enable_copy?: boolean;60copy_tip?: string;61open_tip?: string;62omit_errors?: boolean;63}6465const RECOPY_INIT: Record<Steps, false> = {66Assign: false,67Collect: false,68"Peer Assign": false,69Return: false,70"Peer Collect": false,71} as const;7273function useRecopy(): [74typeof RECOPY_INIT,75(key: Steps, value: boolean) => void,76] {77const [recopy, set_recopy] = useState<typeof RECOPY_INIT>(RECOPY_INIT);78function set(key: Steps, value: boolean) {79set_recopy({ ...recopy, [key]: value });80}81return [recopy, set];82}8384export function StudentAssignmentInfo({85name,86title,87student,88assignment,89grade = "",90comments = "",91info,92nbgrader_scores,93nbgrader_score_ids,94is_editing,95nbgrader_run_info,96}: StudentAssignmentInfoProps) {97const intl = useIntl();98const clicked_nbgrader = useRef<Date>();99const actions = useActions<CourseActions>({ name });100const size = useButtonSize();101const [recopy, set_recopy] = useRecopy();102103function open(104type: AssignmentCopyType,105assignment_id: string,106student_id: string,107) {108return actions.assignments.open_assignment(type, assignment_id, student_id);109}110111function copy(112type: AssignmentCopyType,113assignment_id: string,114student_id: string,115) {116return actions.assignments.copy_assignment(type, assignment_id, student_id);117}118119function stop(120type: AssignmentCopyType,121assignment_id: string,122student_id: string,123) {124actions.assignments.stop_copying_assignment(125assignment_id,126student_id,127type,128);129}130131function set_edited_feedback() {132actions.assignments.update_edited_feedback(133assignment.get("assignment_id"),134student.get("student_id"),135);136}137138function stop_editing() {139actions.assignments.clear_edited_feedback(140assignment.get("assignment_id"),141student.get("student_id"),142);143}144145function render_grade() {146if (is_editing) {147return (148<MarkdownInput149placeholder="Grade..."150value={grade || ""}151onBlur={(grade) => {152actions.assignments.set_grade(153assignment.get("assignment_id"),154student.get("student_id"),155grade,156);157}}158onShiftEnter={() => stop_editing()}159height="3em"160hideHelp161style={{ margin: "5px 0" }}162autoFocus163/>164);165} else {166const text = intl.formatMessage(167{168id: "course.student-assignment-info.grade.label",169defaultMessage: `{show, select, true {Grade: {grade}} other {Enter grade...}}`,170description: "Grade of an assignment in an online course",171},172{ grade, show: !!((grade ?? "").trim() || (comments ?? "").trim()) },173);174175return (176<Button177key="edit"178onClick={() => set_edited_feedback()}179disabled={is_editing}180size={size}181>182{text}183</Button>184);185}186}187188function render_comments() {189if (!is_editing) {190if (!comments?.trim()) return;191return (192<div style={{ width: "100%", paddingRight: "5px" }}>193<Markdown194value={comments}195style={{196width: "100%",197maxHeight: "4em",198overflowY: "auto",199padding: "5px",200border: "1px solid lightgray",201cursor: "pointer",202display: "inline-block",203}}204onClick={() => set_edited_feedback()}205/>206</div>207);208} else {209return (210<MarkdownInput211placeholder="Optional markdown comments..."212value={comments || ""}213onBlur={(comment) => {214actions.assignments.set_comment(215assignment.get("assignment_id"),216student.get("student_id"),217comment,218);219}}220onShiftEnter={() => stop_editing()}221height="7em"222hideHelp223/>224);225}226}227228function render_nbgrader_scores() {229if (!nbgrader_scores) return;230return (231<div>232<NbgraderScores233show_all={is_editing}234set_show_all={() => set_edited_feedback()}235nbgrader_scores={nbgrader_scores}236nbgrader_score_ids={nbgrader_score_ids}237name={name}238student_id={student.get("student_id")}239assignment_id={assignment.get("assignment_id")}240/>241{render_run_nbgrader("Run nbgrader again")}242</div>243);244}245246function render_run_nbgrader(label: JSX.Element | string) {247let running = false;248if (nbgrader_run_info != null) {249const t = nbgrader_run_info.get(250assignment.get("assignment_id") + "-" + student.get("student_id"),251);252if (t && webapp_client.server_time() - t <= 1000 * 60 * 10) {253// Time starting is set and it's also within the last few minutes.254// This "few minutes" is just in case -- we probably shouldn't need255// that at all ever, but it could make cocalc state usable in case of256// weird issues, I guess). User could also just close and re-open257// the course file, which resets this state completely.258running = true;259}260}261label = running ? (262<span>263{" "}264<Spin /> Running nbgrader265</span>266) : (267<span>{label}</span>268);269270return (271<div style={{ marginTop: "5px" }}>272<Button273key="nbgrader"274disabled={running}275size={size}276onClick={() => {277if (278clicked_nbgrader.current != null &&279webapp_client.server_time() -280clicked_nbgrader.current.valueOf() <=2813000282) {283// User *just* clicked, and we want to avoid double click284// running nbgrader twice.285return;286}287288clicked_nbgrader.current = new Date();289actions.assignments.run_nbgrader_for_one_student(290assignment.get("assignment_id"),291student.get("student_id"),292);293}}294>295<Icon name="graduation-cap" /> {label}296</Button>297</div>298);299}300301function render_nbgrader() {302if (nbgrader_scores) {303return render_nbgrader_scores();304}305if (!assignment.get("nbgrader") || assignment.get("skip_grading")) return;306307return render_run_nbgrader("Run nbgrader");308}309310function render_save_button() {311if (!is_editing) return;312return (313<Button key="save" size={size} onClick={() => stop_editing()}>314Save315</Button>316);317}318319function render_last_time(time: string | number | Date) {320return (321<div key="time" style={{ color: "#666" }}>322<BigTime date={time} />323</div>324);325}326327function render_open_recopy_confirm(328step: Steps,329copy: Function,330copy_tip: string,331placement,332) {333if (recopy[step]) {334const v: JSX.Element[] = [];335v.push(336<Button337key="copy_cancel"338size={size}339onClick={() => set_recopy(step, false)}340>341{intl.formatMessage(labels.cancel)}342</Button>,343);344v.push(345<Button346key="recopy_confirm"347danger348size={size}349onClick={() => {350set_recopy(step, false);351copy();352}}353>354<Icon355name="share-square"356rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}357/>{" "}358<FormattedMessage359id="course.student-assignment-info.recopy_confirm.label"360defaultMessage={`Yes, {activity} again`}361description={"Confirm an activity, like 'assign', 'collect', ..."}362values={{ activity: step_intl(step, false).toLowerCase() }}363/>364</Button>,365);366if (step.toLowerCase() === "assign") {367// inline-block because buttons above are float:left368v.push(369<div370key="what-happens"371style={{ margin: "5px", display: "inline-block" }}372>373<a374target="_blank"375href="https://doc.cocalc.com/teaching-tips_and_tricks.html#how-exactly-are-assignments-copied-to-students"376>377{intl.formatMessage({378id: "course.student-assignment-info.recopy.what_happens",379defaultMessage: "What happens when I assign again?",380description:381"Asking the question, what happens if all files are transferred to all students in an online course once again.",382})}383</a>384</div>,385);386}387return <Space wrap>{v}</Space>;388} else {389return (390<Button391key="copy"392type="dashed"393size={size}394onClick={() => set_recopy(step, true)}395>396<Tip title={step} placement={placement} tip={<span>{copy_tip}</span>}>397<Icon398name="share-square"399rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}400/>{" "}401{step_intl(step, false)}...402</Tip>403</Button>404);405}406}407408function render_open_recopy(409step: Steps,410open,411copy,412copy_tip: string,413open_tip: string,414) {415const placement = step === "Return" ? "left" : "right";416return (417<div key="open_recopy">418{render_open_recopy_confirm(step, copy, copy_tip, placement)}419<Gap />420<Button key="open" size={size} onClick={open}>421<Tip title="Open assignment" placement={placement} tip={open_tip}>422<Icon name="folder-open" /> {intl.formatMessage(labels.open)}423</Tip>424</Button>425</div>426);427}428429function step_intl(step: Steps, active: boolean): string {430return intl.formatMessage(active ? STEPS_INTL_ACTIVE : STEPS_INTL, {431step: STEP_NAMES.indexOf(step),432});433}434435function render_open_copying(step: Steps, open, stop) {436return (437<Space key="open_copying" wrap>438<Button key="copy" disabled={true} size={size}>439<Spin /> {step_intl(step, true)}440</Button>441<Button key="stop" danger onClick={stop} size={size}>442{intl.formatMessage(labels.cancel)} <Icon name="times" />443</Button>444<Button key="open" onClick={open} size={size}>445<Icon name="folder-open" /> {intl.formatMessage(labels.open)}446</Button>447</Space>448);449}450451function render_copy(step: Steps, copy: () => void, copy_tip: string) {452let placement;453if (step === "Return") {454placement = "left";455}456return (457<Tip key="copy" title={step} tip={copy_tip} placement={placement}>458<Button onClick={copy} size={size}>459<Icon460name="share-square"461rotate={step.indexOf("ollect") !== -1 ? "180" : undefined}462/>{" "}463{step_intl(step, false)}464</Button>465</Tip>466);467}468469function render_error(step: Steps, error) {470if (typeof error !== "string") {471error = to_json(error);472}473// We search for two different error messages, since different errors happen in474// KuCalc versus other places cocalc runs. It depends on what is doing the copy.475if (476error.indexOf("No such file or directory") !== -1 ||477error.indexOf("ENOENT") != -1478) {479error = `The student might have renamed or deleted the directory that contained their assignment. Open their project and see what happened. If they renamed it, you could rename it back, then collect the assignment again.\n${error}`;480} else {481error = `Try to ${step.toLowerCase()} again:\n` + error;482}483return (484<ShowError485key="error"486error={error}487style={{488marginTop: "5px",489maxHeight: "140px",490overflow: "auto",491display: "block",492}}493/>494);495}496497function Status({498step,499type,500data = {},501enable_copy = false,502copy_tip = "",503open_tip = "",504omit_errors = false,505}: RenderLastProps): JSX.Element {506const do_open = () => open(type, info.assignment_id, info.student_id);507const do_copy = () => copy(type, info.assignment_id, info.student_id);508const do_stop = () => stop(type, info.assignment_id, info.student_id);509const v: JSX.Element[] = [];510if (enable_copy) {511if (webapp_client.server_time() - (data.start ?? 0) < COPY_TIMEOUT_MS) {512v.push(render_open_copying(step, do_open, do_stop));513} else if (data.time) {514v.push(515render_open_recopy(516step,517do_open,518do_copy,519copy_tip as string,520open_tip as string,521),522);523} else {524v.push(render_copy(step, do_copy, copy_tip as string));525}526}527if (data.time) {528v.push(render_last_time(data.time));529}530if (data.error && !omit_errors) {531v.push(render_error(step, data.error));532}533return <>{v}</>;534}535536let show_grade_col, show_return_graded;537const peer_grade: boolean = !!assignment.getIn(["peer_grade", "enabled"]);538const skip_grading: boolean = !!assignment.get("skip_grading");539const skip_assignment: boolean = !!assignment.get("skip_assignment");540const skip_collect: boolean = !!assignment.get("skip_collect");541if (peer_grade) {542show_grade_col = !skip_grading && info.last_peer_collect;543show_return_graded = grade || (skip_grading && info.last_peer_collect);544} else {545show_grade_col = (!skip_grading && info.last_collect) || skip_collect;546show_return_graded =547grade ||548(skip_grading && info.last_collect) ||549(skip_grading && skip_collect);550}551552const width = peer_grade ? 4 : 6;553554function render_assignment_col() {555return (556<Col md={width} key="last_assignment">557<Status558step="Assign"559data={info.last_assignment}560type="assigned"561enable_copy={true}562copy_tip={intl.formatMessage({563id: "course.student-assignment-info.assignment_col.copy.tooltip",564defaultMessage: `Copy the assignment from your project to this student's project so they can do their homework.`,565description: "files of a student in an online course",566})}567open_tip={intl.formatMessage({568id: "course.student-assignment-info.assignment_col.open.tooltip",569defaultMessage: `Open the student's copy of this assignment directly in their project.570You will be able to see them type, chat with them, leave them hints, etc.`,571description: "files of a student in an online course",572})}573omit_errors={skip_assignment}574/>575</Col>576);577}578579function render_collect_col() {580return (581<Col md={width} key="last_collect">582{skip_assignment ||583!(info.last_assignment != null584? info.last_assignment.error585: undefined) ? (586<Status587step="Collect"588data={info.last_collect}589type="collected"590enable_copy={info.last_assignment != null || skip_assignment}591copy_tip={intl.formatMessage({592id: "course.student-assignment-info.collect_col.copy.tooltip",593defaultMessage:594"Copy the assignment from your student's project back to your project so you can grade their work.",595description: "files of a student in an online course",596})}597open_tip={intl.formatMessage({598id: "course.student-assignment-info.collect_col.open.tooltip",599defaultMessage:600"Open the copy of your student's work in your own project, so that you can grade their work.",601description: "files of a student in an online course",602})}603omit_errors={skip_collect}604/>605) : undefined}606</Col>607);608}609610function render_peer_assign_col() {611if (!peer_grade) return;612if (!info.peer_assignment) return;613if (info.last_collect?.error != null) return;614return (615<Col md={4} key="peer_assign">616<Status617step="Peer Assign"618data={info.last_peer_assignment}619type={"peer-assigned"}620enable_copy={info.last_collect != null}621copy_tip={intl.formatMessage({622id: "course.student-assignment-info.peer_assign_col.copy.tooltip",623defaultMessage:624"Copy collected assignments from your project to this student's project so they can grade them.",625description: "files of a student in an online course",626})}627open_tip={intl.formatMessage({628id: "course.student-assignment-info.peer_assign_col.open.tooltip",629defaultMessage:630"Open the student's copies of this assignment directly in their project, so you can see what they are peer grading.",631description: "files of a student in an online course",632})}633/>634</Col>635);636}637638function render_peer_collect_col() {639if (!peer_grade) return;640if (!info.peer_collect) return;641return (642<Col md={4} key="peer_collect">643<Status644step="Peer Collect"645data={info.last_peer_collect}646type="peer-collected"647enable_copy={info.last_peer_assignment != null}648copy_tip={intl.formatMessage({649id: "course.student-assignment-info.peer_collect_col.copy.tooltip",650defaultMessage:651"Copy the peer-graded assignments from various student projects back to your project so you can assign their official grade.",652description: "files of a student in an online course",653})}654open_tip={intl.formatMessage({655id: "course.student-assignment-info.peer_collect_col.open.tooltip",656defaultMessage:657"Open your copy of your student's peer grading work in your own project, so that you can grade their work.",658659description: "files of a student in an online course",660})}661/>662</Col>663);664}665666function render_grade_col() {667// {render_enter_grade()}668return (669<Col md={width} key="grade">670{show_grade_col && (671<div>672{render_save_button()}673{render_grade()}674{render_comments()}675{render_nbgrader()}676</div>677)}678</Col>679);680}681682function render_return_graded_col() {683return (684<Col md={width} key="return_graded">685{show_return_graded ? (686<Status687step="Return"688data={info.last_return_graded}689type="graded"690enable_copy={info.last_collect != null || skip_collect}691copy_tip={intl.formatMessage({692id: "course.student-assignment-info.graded_col.copy.tooltip",693defaultMessage: `Copy the graded assignment back to your student's project.`,694description: "files of a student in an online course",695})}696open_tip={intl.formatMessage({697id: "course.student-assignment-info.graded_col.open.tooltip",698defaultMessage: `Open the copy of your student's work that you returned to them.699This opens the returned assignment directly in their project.`,700description: "the files of a student in an online course",701})}702/>703) : undefined}704</Col>705);706}707708return (709<div>710<Row711style={{712borderTop: "1px solid #aaa",713paddingTop: "5px",714paddingBottom: "5px",715}}716>717<Col md={4} key="title">718{title}719</Col>720<Col md={20} key="rest">721<Row>722{render_assignment_col()}723{render_collect_col()}724{render_peer_assign_col()}725{render_peer_collect_col()}726{render_grade_col()}727{render_return_graded_col()}728</Row>729</Col>730</Row>731</div>732);733}734735736