Path: blob/master/src/packages/frontend/course/assignments/assignment.tsx
5837 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import {6Alert,7Button,8Card,9Col,10ConfigProvider,11Divider,12Input,13Popconfirm,14Popover,15Row,16Switch,17Space,18} from "antd";19import { ReactElement, ReactNode, useEffect, useState } from "react";20import { DebounceInput } from "react-debounce-input";21import { FormattedMessage, useIntl } from "react-intl";22import { AppRedux, useActions } from "@cocalc/frontend/app-framework";23import {24DateTimePicker,25Icon,26Loading,27Tip,28} from "@cocalc/frontend/components";29import MultiMarkdownInput from "@cocalc/frontend/editors/markdown-input/multimode";30import StaticMarkdown from "@cocalc/frontend/editors/slate/static-markdown";31import { labels } from "@cocalc/frontend/i18n";32import { capitalize, trunc_middle } from "@cocalc/util/misc";33import { CourseActions } from "../actions";34import { BigTime, Progress, StudentAssignmentInfoHeader } from "../common";35import { nbgrader_status } from "../nbgrader/util";36import { webapp_client } from "@cocalc/frontend/webapp-client";37import type {38AssignmentRecord,39CourseStore,40IsGradingMap,41NBgraderRunInfo,42} from "../store";43import * as styles from "../styles";44import { AssignmentCopyStep, AssignmentStatus } from "../types";45import {46step_direction,47step_ready,48step_verb,49STEPS,50useButtonSize,51} from "../util";52import { StudentListForAssignment } from "./assignment-student-list";53import { ConfigurePeerGrading } from "./configure-peer";54import { STUDENT_SUBDIR } from "./consts";55import { ComputeServerButton } from "../compute";5657interface AssignmentProps {58active_feedback_edits: IsGradingMap;59assignment: AssignmentRecord;60background?: string;61expand_peer_config?: boolean;62frame_id?: string;63is_expanded?: boolean;64name: string;65nbgrader_run_info?: NBgraderRunInfo;66project_id: string;67redux: AppRedux;68students: object;69user_map: object;70}7172export function Assignment({73active_feedback_edits,74assignment,75background,76expand_peer_config,77frame_id,78is_expanded,79name,80nbgrader_run_info,81project_id,82redux,83students,84user_map,85}: AssignmentProps) {86const intl = useIntl();87const size = useButtonSize();88const assignmentId = assignment.get("assignment_id");89const noteProp = assignment.get("note") ?? "";90const [noteValue, setNoteValue] = useState<string>(noteProp);91const [noteEditing, setNoteEditing] = useState<boolean>(false);9293useEffect(() => {94setNoteValue(noteProp);95setNoteEditing(false);96}, [assignmentId]);9798useEffect(() => {99if (!noteEditing) {100setNoteValue(noteProp);101}102}, [noteProp, noteEditing]);103104useEffect(() => {105if (is_peer_graded()) {106for (const step of ["assignment", "collect"] as const) {107if (assignment.get(`skip_${step}` as any)) {108actions.assignments.set_skip(assignmentId, step, false);109}110}111}112}, [assignmentId, assignment.getIn(["peer_grade", "enabled"])]);113114useEffect(() => {115if (assignment.get("nbgrader") && is_peer_graded()) {116actions.assignments.set_peer_grade(assignmentId, { enabled: false });117setPeerDisabledForNbgrader(true);118}119}, [120assignmentId,121assignment.get("nbgrader"),122assignment.getIn(["peer_grade", "enabled"]),123]);124125const [126copy_assignment_confirm_overwrite,127set_copy_assignment_confirm_overwrite,128] = useState<boolean>(false);129const [peerDisabledForNbgrader, setPeerDisabledForNbgrader] = useState(false);130const [131copy_assignment_confirm_overwrite_text,132set_copy_assignment_confirm_overwrite_text,133] = useState<string>("");134const [student_search, set_student_search] = useState<string>("");135const [opened_run_all, set_opened_run_all] = useState<136AssignmentCopyStep | "grade" | null137>(null);138const [confirm_all_students, set_confirm_all_students] =139useState<boolean>(false);140const [confirm_sync_grades, set_confirm_sync_grades] =141useState<boolean>(false);142143const actions = useActions<CourseActions>({ name });144145function get_store(): CourseStore {146return actions.get_store();147}148149function is_peer_graded() {150return !!assignment.getIn(["peer_grade", "enabled"]);151}152153function render_due() {154return (155<Space align="center">156<div>Due:</div>157<Tip158placement="top"159title="Set the due date"160tip="Set the due date for the assignment. This changes how the list of assignments is sorted. Note that you must explicitly click a button to collect student assignments when they are due -- they are not automatically collected on the due date. You should also tell students when assignments are due (e.g., at the top of the assignment)."161>162<DateTimePicker163placeholder={"Set Due Date"}164value={assignment.get("due_date")}165onChange={date_change}166/>167</Tip>168</Space>169);170}171172function date_change(date): void {173actions.assignments.set_due_date(174assignment.get("assignment_id"),175date != null ? date.toISOString() : undefined,176);177}178179function render_note() {180return (181<Space182key="note"183align="start"184style={{185display: "grid",186gridTemplateColumns: "auto 1fr",187}}188>189<Tip190title="Notes about this assignment"191tip="Record notes about this assignment here. These notes are only visible to you, not to your students. Put any instructions to students about assignments in a file in the directory that contains the assignment."192>193<Button194icon={<Icon name="pencil" />}195type={noteEditing ? "primary" : "default"}196onClick={() => {197if (noteEditing) {198actions.assignments.set_assignment_note(199assignmentId,200noteValue,201);202}203setNoteEditing(!noteEditing);204}}205>206{noteEditing ? "Done" : "Notes:"}207</Button>208</Tip>209<div style={{ minWidth: 0, width: "100%" }}>210{noteEditing ? (211<MultiMarkdownInput212value={noteValue}213onChange={(value: string) => setNoteValue(value)}214placeholder="Private notes about this assignment (not visible to students)"215height="200px"216minimal217enableUpload={false}218/>219) : (220<StaticMarkdown value={noteValue ?? ""} />221)}222</div>223</Space>224);225}226227function render_export_file_use_times() {228return (229<Tip230title="Export when students used files"231tip="Export a JSON file containing extensive information about exactly when students have opened or edited files in this assignment. 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."232>233<Button234onClick={() =>235actions.export.file_use_times(assignment.get("assignment_id"))236}237>238File Use Times239</Button>240</Tip>241);242}243244function render_export_assignment() {245return (246<Tip247title="Export collected student files"248tip="Export all student work to files in a single directory that are easy to grade or archive outside of CoCalc. Any Jupyter notebooks or Sage worksheets are first converted to PDF (if possible), and all files are renamed with the student as a filename prefix."249>250<Button251onClick={() =>252actions.assignments.export_collected(253assignment.get("assignment_id"),254)255}256>257Export258</Button>259</Tip>260);261}262263function render_no_content() {264if (assignment.get("deleted")) {265// no point266return null;267}268return (269<div style={{ margin: "15px auto", maxWidth: "800px", fontSize: "12pt" }}>270There are no files in this assignment yet. Please{" "}271<a onClick={open_assignment_path}>open the directory</a> for this272assignment, then create, upload, or copy any content you want into that273directory. {intl.formatMessage(labels.you)} will then be able to send it274to all of your students.275</div>276);277}278279function render_more_header(num_files: number) {280const status: AssignmentStatus | undefined =281get_store().get_assignment_status(assignment.get("assignment_id"));282if (status == null) {283return <Loading key="loading_more" />;284}285const v: ReactElement<any>[] = [];286const stackSize = size === "small" ? "small" : "middle";287288v.push(289<Space290key="header-stack"291direction="vertical"292size={stackSize}293style={{ width: "100%" }}294>295<Row gutter={[8, 4]} align="top" justify="space-between">296<Col md={16}>297<Space wrap size={[12, 6]}>298{render_open_button()}299{render_due()}300{render_peer_button()}301<ComputeServerButton actions={actions} unit={assignment as any} />302</Space>303</Col>304<Col md={8} style={{ marginLeft: "auto" }}>305<Space306wrap307size={[12, 6]}308style={{ width: "100%", justifyContent: "flex-end" }}309>310{render_export_file_use_times()}311{render_export_assignment()}312{render_delete_button()}313</Space>314</Col>315</Row>316317{peerDisabledForNbgrader ? (318<div style={{ marginTop: 8 }}>319<Alert320type="warning"321showIcon322closable323onClose={() => setPeerDisabledForNbgrader(false)}324message="Peer grading was disabled because nbgrader notebooks were detected. Remove nbgrader metadata to re-enable peer grading."325/>326</div>327) : null}328329{expand_peer_config ? (330<ConfigurePeerGrading actions={actions} assignment={assignment} />331) : null}332333{render_note()}334<Divider style={{ borderTopWidth: 3, margin: 0 }} />335336{(() => {337const peer = is_peer_graded();338339if (num_files === 0) return null;340341const actions: Partial<342Record<AssignmentCopyStep | "grade", ReactElement<any>[]>343> = {};344const progress: Partial<345Record<AssignmentCopyStep | "grade", ReactElement<any>>346> = {};347348function add_action(349step: AssignmentCopyStep | "grade",350element: ReactElement<any>,351) {352actions[step] = [...(actions[step] ?? []), element];353}354355const renderedMap: Partial<Record<AssignmentCopyStep, boolean>> = {};356357for (const name of STEPS(peer)) {358const rendered = render_step_run_all(name, status);359// squeeze in the skip grading button (don't add it to STEPS!)360if (rendered != null) {361renderedMap[name] = true;362if (Array.isArray(rendered)) {363const buttons = rendered.filter(364(elem) => elem?.type !== Progress,365);366const prog = rendered.find(367(elem) => elem?.type === Progress,368) as ReactElement | undefined;369if (buttons.length > 0) {370add_action(name, <span key={name}>{buttons}</span>);371}372if (prog) {373progress[name] = prog;374}375} else {376add_action(name, <span key={name}>{rendered}</span>);377}378}379380if (!peer) {381if (rendered && name === "assignment") {382add_action(383"assignment",384<span key="skip-assignment">385{render_skip_switch("assignment")}386</span>,387);388} else if (rendered && name === "collect") {389add_action(390"collect",391<span key="skip-collect">392{render_skip_switch("collect")}393</span>,394);395}396}397}398399const gradeAction = render_grade_run_all(status);400if (gradeAction) {401add_action("grade", gradeAction);402}403404if (status.collect > 0 && renderedMap.collect) {405add_action(406"grade",407<span key="skip-grade">408{render_skip_switch("grading", status.collect === 0)}409</span>,410);411}412413if (414renderedMap.peer_assignment &&415status.peer_assignment != null &&416progress.peer_assignment == null417) {418progress["peer_assignment"] = (419<Progress420key="progress-peer-assign"421done={status.peer_assignment}422not_done={status.not_peer_assignment}423step="peer assigned"424/>425);426}427428if (429renderedMap.peer_collect &&430status.peer_collect != null &&431progress.peer_collect == null432) {433progress["peer_collect"] = (434<Progress435key="progress-peer-collect"436done={status.peer_collect}437not_done={status.not_peer_collect}438step="peer collected"439/>440);441}442443if (renderedMap.return_graded) {444progress["return_graded"] = (445<Progress446key="progress-return"447done={status.return_graded}448not_done={status.not_return_graded}449step="returned"450/>451);452}453454return (455<>456<StudentAssignmentInfoHeader457key="header"458title="Student"459peer_grade={peer}460mode="assignment"461actions={actions}462progress={progress}463filter={464<DebounceInput465debounceTimeout={500}466element={Input as any}467placeholder={"Filter students..."}468value={student_search}469onChange={(e) => set_student_search(e.target.value)}470/>471}472/>473</>474);475})()}476</Space>,477);478/* The whiteSpace:'normal' here is because we put this in an479antd Card title, which has line wrapping disabled. */480return <div style={{ whiteSpace: "normal" }}>{v}</div>;481}482483function render_more() {484const num_files = assignment.get("listing")?.size ?? 0;485let body;486if (num_files == 0) {487body = render_no_content();488} else {489body = (490<StudentListForAssignment491redux={redux}492frame_id={frame_id}493name={name}494assignment={assignment}495students={students}496user_map={user_map}497active_feedback_edits={active_feedback_edits}498nbgrader_run_info={nbgrader_run_info}499search={student_search}500/>501);502}503return (504<Row key="more">505<Col sm={24}>506<ConfigProvider componentSize={size}>507<Card>508{render_more_header(num_files)}509{body}510</Card>511</ConfigProvider>512</Col>513</Row>514);515}516517function open_assignment_path(): void {518if (assignment.get("listing")?.size == 0) {519// there are no files yet, so we *close* the assignment520// details panel. This is just **a hack** so that the user521// has to re-open it after adding files, which will trigger522// updating the directory listing, hence show the rest523// of the assignment info. The alternative would be524// polling the directory or watching listings, which is525// a lot more work to properly implement.526actions.toggle_item_expansion(527"assignment",528assignment.get("assignment_id"),529);530}531return redux532.getProjectActions(project_id)533.open_directory(assignment.get("path"));534}535536function render_open_button() {537return (538<Tip539key="open"540title={541<span>542<Icon name="folder-open" /> Open Folder543</span>544}545tip="Open the directory in the current project that contains the original files for this assignment. Edit files in this folder to create the content that your students will see when they receive an assignment."546>547<Button onClick={open_assignment_path}>548<Icon name="folder-open" /> {intl.formatMessage(labels.open)}549</Button>550</Tip>551);552}553554function render_step_popover(555step: AssignmentCopyStep,556opts: {557type: "primary" | "default";558content: ReactNode;559onOpen?: () => void;560onClose?: () => void;561},562) {563const open = opened_run_all === step;564const handleOpenChange = (next: boolean) => {565set_opened_run_all(next ? step : null);566set_confirm_all_students(false);567set_confirm_sync_grades(false);568if (next) {569opts.onOpen?.();570} else {571opts.onClose?.();572}573};574return render_run_all_popover({575key: step,576open,577onOpenChange: handleOpenChange,578type: opts.type,579content: opts.content,580ariaLabel: run_all_button_aria_label(step),581});582}583584function render_run_all_popover(opts: {585key: AssignmentCopyStep | "grade";586open: boolean;587onOpenChange: (next: boolean) => void;588type: "primary" | "default";589content: ReactNode;590ariaLabel: string;591}) {592return (593<Popover594key={opts.key}595placement="bottom"596trigger="click"597open={opts.open}598onOpenChange={opts.onOpenChange}599content={opts.content}600overlayInnerStyle={{ maxWidth: 545 }}601>602<span style={{ display: "inline-block" }}>603<Button604type={opts.type}605size="small"606icon={<Icon name="forward" />}607aria-label={opts.ariaLabel}608onClick={() => opts.onOpenChange(true)}609/>610</span>611</Popover>612);613}614615function render_grade_popover(opts: {616type: "primary" | "default";617content: ReactNode;618}) {619const handleOpenChange = (next: boolean) => {620set_opened_run_all(next ? "grade" : null);621set_confirm_all_students(false);622set_confirm_sync_grades(false);623};624return render_run_all_popover({625key: "grade",626open: opened_run_all === "grade",627onOpenChange: handleOpenChange,628type: opts.type,629content: opts.content,630ariaLabel: run_all_button_aria_label("grade"),631});632}633634function render_assignment_button(status) {635const has_new = status.not_assignment > 0;636const type = run_all_button_type(637"assignment",638status,639has_new,640);641642return [643render_step_popover("assignment", {644type,645content: render_step_confirm("assignment", status),646onOpen: () => {647const assignment_id: string | undefined =648assignment.get("assignment_id");649actions.assignments.update_listing(assignment_id);650},651onClose: () => {652set_copy_assignment_confirm_overwrite(false);653},654}),655<Progress656key="progress"657done={status.assignment}658not_done={status.not_assignment}659step="assigned"660skipped={assignment.get("skip_assignment")}661/>,662];663}664665function render_step_confirm(step, status) {666return render_copy_confirm(step, status);667}668669function step_complete_or_skipped(670step: AssignmentCopyStep,671status: AssignmentStatus,672) {673if (step === "assignment" && assignment.get("skip_assignment")) {674return true;675}676if (step === "collect" && assignment.get("skip_collect")) {677return true;678}679const not_done = status[`not_${step}`];680if (not_done == null) return false;681return not_done === 0;682}683684function previous_steps_complete(685step: AssignmentCopyStep,686status: AssignmentStatus,687) {688const peer = is_peer_graded();689for (const s of STEPS(peer)) {690if (s === step) break;691if (!step_complete_or_skipped(s, status)) {692return false;693}694}695return true;696}697698function run_all_button_type(699step: AssignmentCopyStep,700status: AssignmentStatus,701has_new: boolean,702): "primary" | "default" {703const prev_complete = previous_steps_complete(step, status);704if (has_new && prev_complete) {705return "primary";706}707return "default";708}709710function run_all_button_aria_label(711step: AssignmentCopyStep | "grade",712): string {713switch (step) {714case "assignment":715return "Assign to all students options";716case "collect":717return "Collect from all students options";718case "peer_assignment":719return "Assign for peer grading options";720case "peer_collect":721return "Collect peer grading options";722case "return_graded":723return "Return to all students options";724case "grade":725return "Autograde options";726default:727return "Run all options";728}729}730731function is_nbgrader_running(): boolean {732if (nbgrader_run_info == null) return false;733const t = nbgrader_run_info.get(assignmentId);734// Time starting is set and it's also within the last few minutes.735// This "few minutes" is just in case -- we probably shouldn't need736// that at all ever, but it could make cocalc state usable in case of737// weird issues, I guess). User could also just close and re-open738// the course file, which resets this state completely.739return webapp_client.server_time() - (t ?? 0) <= 1000 * 60 * 10;740}741742function render_nbgrader_run_all() {743const assignment_id = assignment.get("assignment_id");744if (assignment_id == null) return null;745const nbgrader = nbgrader_status(assignment);746const total = nbgrader.attempted + nbgrader.not_attempted;747const todo = nbgrader.not_attempted + nbgrader.failed;748const running = is_nbgrader_running();749const show_remaining =750todo > 0 && !confirm_all_students && !confirm_sync_grades;751const alertType = confirm_all_students || confirm_sync_grades752? "error"753: show_remaining754? "warning"755: "success";756const message = (757<Space direction="vertical">758<span>Autograde this assignment using nbgrader for</span>759{show_remaining && (760<Button761disabled={running}762type="primary"763onClick={() => {764actions.assignments.run_nbgrader_for_all_students(765assignment_id,766true,767);768set_opened_run_all(null);769}}770>771The {todo} student{todo > 1 ? "s" : ""} not already autograded772</Button>773)}774{nbgrader.attempted > 0 && !confirm_sync_grades && (775<Button776danger777disabled={running || confirm_all_students}778onClick={() => {779set_confirm_all_students(true);780set_confirm_sync_grades(false);781}}782>783All {total} students...784</Button>785)}786{confirm_all_students && (787<Space direction="vertical">788<div>Are you sure you want to autograde ALL {total} students?</div>789<Space>790<Button791danger792type="primary"793disabled={running}794onClick={() => {795actions.assignments.run_nbgrader_for_all_students(796assignment_id,797);798set_opened_run_all(null);799set_confirm_all_students(false);800}}801>802Autograde all {total} students803</Button>804<Button805onClick={() => set_confirm_all_students(false)}806disabled={running}807>808Back809</Button>810</Space>811</Space>812)}813{!confirm_all_students && (814<Button815disabled={running || confirm_sync_grades}816onClick={() => {817set_confirm_sync_grades(true);818set_confirm_all_students(false);819}}820>821Sync grades...822</Button>823)}824{confirm_sync_grades && (825<Space direction="vertical">826<div>827Copy the nbgrader grades to be the assigned grades for all828students, even if there are ungraded manual problems, errors or829other issues? You probably don't need to do this.830</div>831<Space>832<Button833danger834type="primary"835disabled={running}836onClick={() => {837actions.assignments.set_nbgrader_scores_for_all_students({838assignment_id,839force: true,840commit: true,841});842set_opened_run_all(null);843}}844>845Sync grades for all students846</Button>847<Button848onClick={() => set_confirm_sync_grades(false)}849disabled={running}850>851Back852</Button>853</Space>854</Space>855)}856</Space>857);858return <Alert type={alertType} message={message} />;859}860861function render_copy_assignment_confirm_overwrite(step) {862if (!copy_assignment_confirm_overwrite) {863return;864}865const do_it = () => {866copy_assignment(step, false, true);867set_copy_assignment_confirm_overwrite(false);868set_copy_assignment_confirm_overwrite_text("");869};870return (871<Space direction="vertical">872Type in "OVERWRITE" in the box below if you are sure you want to873overwrite any work the students may have done.874<Input875autoFocus876onChange={(e) =>877set_copy_assignment_confirm_overwrite_text((e.target as any).value)878}879/>880<Button881disabled={copy_assignment_confirm_overwrite_text !== "OVERWRITE"}882icon={<Icon name="exclamation-triangle" />}883danger884type="primary"885onClick={do_it}886>887Confirm replacing files888</Button>889</Space>890);891}892893function copy_assignment(894step,895new_only: boolean,896overwrite: boolean = false,897) {898// assign assignment to all (non-deleted) students899const assignment_id: string | undefined = assignment.get("assignment_id");900if (assignment_id == null) throw Error("bug");901switch (step) {902case "assignment":903actions.assignments.copy_assignment_to_all_students(904assignment_id,905new_only,906overwrite,907);908break;909case "collect":910actions.assignments.copy_assignment_from_all_students(911assignment_id,912new_only,913);914break;915case "peer_assignment":916actions.assignments.peer_copy_to_all_students(assignment_id, new_only);917break;918case "peer_collect":919actions.assignments.peer_collect_from_all_students(920assignment_id,921new_only,922);923break;924case "return_graded":925actions.assignments.return_assignment_to_all_students(926assignment_id,927new_only,928);929break;930default:931console.log(`BUG -- unknown step: ${step}`);932}933set_confirm_all_students(false);934set_opened_run_all(null);935}936937function render_skip_switch(938step: "assignment" | "collect" | "grading",939disabled?: boolean,940) {941const skipped = assignment.get(`skip_${step}` as any);942return (943<Tip944title="Skip step in workflow"945tip="Toggle to allow proceeding to the next step without completing this one."946>947<Switch948checked={!!skipped}949onChange={() =>950actions.assignments.set_skip(951assignment.get("assignment_id"),952step,953!skipped,954)955}956checkedChildren="Skip"957unCheckedChildren="Skip"958size="small"959disabled={disabled}960/>961</Tip>962);963}964965function copy_confirm_all_caution(step: AssignmentCopyStep) {966const caution = "CAUTION: All files will be copied again.";967const it_will =968"it will get copied to a backup file ending in a tilde (~), or possibly only be available in snapshots.";969switch (step) {970case "assignment":971return (972<span>973{caution} If you updated a file that a student has also worked on,{" "}974{it_will} Select "Replace student files!" if you do <b>not</b> want975to create any backups and want to <b>delete</b> all other files in976the assignment folder of student projects.{" "}977<a978rel="noopener noreferrer"979target="_blank"980href="https://doc.cocalc.com/teaching-tips_and_tricks.html#how-exactly-are-assignments-copied-to-students"981>982Details983</a>984</span>985);986case "collect":987case "peer_collect":988return `${caution} If you have graded or edited a file that a student has updated, ${it_will}`;989case "peer_assignment":990return `${caution} If a student worked on a previously assigned file, ${it_will}`;991case "return_graded":992return `${caution} If a student edited a previously returned file, ${it_will}`;993}994}995996function render_copy_confirm_overwrite_all(step: AssignmentCopyStep) {997return (998<Space direction="vertical" key={"copy_confirm_overwrite_all"}>999{copy_confirm_all_caution(step)}1000<Space>1001<Button1002key={"all"}1003type="primary"1004disabled={copy_assignment_confirm_overwrite}1005onClick={() => copy_assignment(step, false)}1006>1007Yes, do it (with backup)1008</Button>1009{step === "assignment" ? (1010<Button1011key={"all-overwrite"}1012danger1013onClick={() => set_copy_assignment_confirm_overwrite(true)}1014disabled={copy_assignment_confirm_overwrite}1015>1016Replace student files!1017</Button>1018) : undefined}1019<Button1020key="back"1021onClick={() => {1022set_confirm_all_students(false);1023set_copy_assignment_confirm_overwrite(false);1024}}1025>1026Back1027</Button>1028</Space>1029{render_copy_assignment_confirm_overwrite(step)}1030</Space>1031);1032}10331034function render_copy_confirm(step: AssignmentCopyStep, status) {1035const not_done = status[`not_${step}`];1036const possible = not_done + status[step];1037const total = get_store().num_students();1038const show_new_button = not_done && !confirm_all_students;1039const alertType = confirm_all_students1040? "error"1041: show_new_button1042? "warning"1043: "success";1044const message = (1045<Space1046direction="vertical"1047style={{ display: "inline-flex", alignItems: "stretch" }}1048>1049{/* Only the student/ subdirectory will be copied to the students. nbgrader docs */}1050{step === "assignment" && assignment.get("has_student_subdir") ? (1051<Alert1052type="info"1053message={1054<span>1055Only the {STUDENT_SUBDIR}/ subdirectory will be copied to the1056students.{" "}1057<a1058rel="noopener noreferrer"1059target="_blank"1060href="https://doc.cocalc.com/teaching-nbgrader.html#student-version"1061>1062nbgrader docs1063</a>1064</span>1065}1066/>1067) : undefined}1068{/* Assign this assignment to */}1069<div>1070{capitalize(step_verb(step))} this assignment {step_direction(step)}1071</div>1072{/* The 15 students not already assigned to */}1073{show_new_button ? (1074<Button1075key="new"1076type="primary"1077onClick={() => copy_assignment(step, true)}1078>1079{not_done === total ? (1080<>All {total} students</>1081) : (1082<>1083The {not_done} student{not_done > 1 ? "s" : ""} not already{" "}1084{step_verb(step)}ed {step_direction(step)}1085</>1086)}1087</Button>1088) : undefined}1089{/* All 19 students... */}1090{not_done !== possible ? (1091<Button1092key="all"1093danger1094disabled={confirm_all_students}1095onClick={() => {1096set_confirm_all_students(true);1097}}1098>1099All {possible} students1100{step_ready(step, possible)}...1101</Button>1102) : undefined}1103{confirm_all_students1104? render_copy_confirm_overwrite_all(step)1105: undefined}1106</Space>1107);1108return (1109<Alert key={`copy_confirm_${step}`} type={alertType} message={message} />1110);1111}11121113function render_step_run_all(state: AssignmentCopyStep, status) {1114switch (state) {1115case "collect":1116return render_collect_button(status);1117case "return_graded":1118return render_return_graded_button(status);1119case "peer_assignment":1120return render_peer_assignment_button(status);1121case "peer_collect":1122return render_peer_collect_button(status);1123case "assignment":1124return render_assignment_button(status);1125}1126}11271128function render_grade_run_all(status) {1129if (!assignment.get("nbgrader") || status.collect === 0) {1130return;1131}1132const nbgrader = nbgrader_status(assignment);1133if (nbgrader.attempted + nbgrader.not_attempted === 0) {1134return;1135}1136const has_new = nbgrader.not_attempted + nbgrader.failed > 0;1137const type = run_all_button_type(1138"return_graded",1139status,1140has_new,1141);1142return render_grade_popover({1143type,1144content: render_nbgrader_run_all(),1145});1146}11471148function render_collect_button(status) {1149if (status.assignment === 0) {1150// no button if nothing ever assigned1151return;1152}1153const has_new = status.not_collect > 0;1154const type = run_all_button_type(1155"collect",1156status,1157has_new,1158);1159return [1160render_step_popover("collect", {1161type,1162content: render_step_confirm("collect", status),1163}),1164<Progress1165key="progress"1166done={status.collect}1167not_done={status.not_collect}1168step="collected"1169skipped={assignment.get("skip_collect")}1170/>,1171];1172}11731174function render_peer_assignment_button(status) {1175// Render the "Peer Assign..." button in the top row, for peer assigning to all1176// students in the course.1177if (status.peer_assignment == null) {1178// not peer graded1179return;1180}1181if (status.not_collect + status.not_assignment > 0) {1182// collect everything before peer grading1183return;1184}1185if (status.collect === 0) {1186// nothing to peer assign1187return;1188}1189const has_new = status.not_peer_assignment > 0;1190const type = run_all_button_type(1191"peer_assignment",1192status,1193has_new,1194);1195return [1196render_step_popover("peer_assignment", {1197type,1198content: render_step_confirm("peer_assignment", status),1199}),1200<Progress1201key="progress"1202done={status.peer_assignment}1203not_done={status.not_peer_assignment}1204step="peer assigned"1205/>,1206];1207}12081209function render_peer_collect_button(status) {1210// Render the "Peer Collect..." button in the top row, for collecting peer grading from all1211// students in the course.1212if (status.peer_collect == null) {1213return;1214}1215if (status.peer_assignment === 0) {1216// haven't even peer assigned anything -- so nothing to collect1217return;1218}1219if (status.not_peer_assignment > 0) {1220// everybody must have received peer assignment, or collecting isn't allowed1221return;1222}1223const has_new = status.not_peer_collect > 0;1224const type = run_all_button_type(1225"peer_collect",1226status,1227has_new,1228);1229return [1230render_step_popover("peer_collect", {1231type,1232content: render_step_confirm("peer_collect", status),1233}),1234<Progress1235key="progress"1236done={status.peer_collect}1237not_done={status.not_peer_collect}1238step="peer collected"1239/>,1240];1241}12421243function render_return_graded_button(status) {1244if (status.collect === 0) {1245// No button if nothing collected.1246return;1247}1248if (status.peer_collect != null && status.peer_collect === 0) {1249// Peer grading enabled, but we didn't collect anything yet1250return;1251}1252if (1253!assignment.get("skip_grading") &&1254status.not_return_graded === 0 &&1255status.return_graded === 01256) {1257// Nothing unreturned and ungraded yet and also nothing returned yet1258return;1259}1260const has_new = status.not_return_graded > 0;1261const type = run_all_button_type(1262"return_graded",1263status,1264has_new,1265);1266return [1267render_step_popover("return_graded", {1268type,1269content: render_step_confirm("return_graded", status),1270}),1271<Progress1272key="progress"1273done={status.return_graded}1274not_done={status.not_return_graded}1275step="returned"1276/>,1277];1278}12791280function delete_assignment() {1281actions.assignments.delete_assignment(assignment.get("assignment_id"));1282}12831284function undelete_assignment() {1285return actions.assignments.undelete_assignment(1286assignment.get("assignment_id"),1287);1288}12891290function render_delete_button() {1291if (assignment.get("deleted")) {1292return (1293<Tip1294key="delete"1295placement="left"1296title={intl.formatMessage({1297id: "course.assignment.undelete.title",1298defaultMessage: "Undelete assignment",1299})}1300tip={intl.formatMessage({1301id: "course.assignment.undelete.tooltip",1302defaultMessage:1303"Make the assignment visible again in the assignment list and in student grade lists.",1304})}1305>1306<Button onClick={undelete_assignment}>1307<Icon name="trash" /> {intl.formatMessage(labels.undelete)}1308</Button>1309</Tip>1310);1311} else {1312return (1313<Popconfirm1314title={1315<div style={{ maxWidth: "400px" }}>1316<FormattedMessage1317id="course.assignment.delete.confirm.info"1318defaultMessage={`<b>Are you sure you want to delete {name}"?</b>1319{br}1320This removes it from the assignment list and student grade lists,1321but does not delete any files off of disk.1322You can undelete an assignment later by showing it using the 'Show deleted assignments' button.`}1323values={{1324name: trunc_middle(assignment.get("path"), 24),1325br: <br />,1326}}1327/>1328</div>1329}1330onConfirm={delete_assignment}1331cancelText={intl.formatMessage(labels.cancel)}1332>1333<Button>1334<Icon name="trash" /> {intl.formatMessage(labels.delete)}...1335</Button>1336</Popconfirm>1337);1338}1339}13401341function render_peer_button() {1342let icon;1343if (is_peer_graded()) {1344icon = "check-square-o";1345} else {1346icon = "square-o";1347}1348const disabledForNbgrader = !!assignment.get("nbgrader");1349const button = (1350<Button1351disabled={expand_peer_config || disabledForNbgrader}1352onClick={() =>1353actions.toggle_item_expansion(1354"peer_config",1355assignment.get("assignment_id"),1356)1357}1358>1359<Icon name={icon} /> Peer Grading...1360</Button>1361);1362if (!disabledForNbgrader) {1363return button;1364}1365return (1366<Tip title="Peer grading is disabled because nbgrader notebooks were detected">1367<span>{button}</span>1368</Tip>1369);1370}13711372function render_summary_due_date() {1373const due_date = assignment.get("due_date");1374if (due_date) {1375return (1376<div style={{ marginTop: "12px" }}>1377Due <BigTime date={due_date} />1378</div>1379);1380}1381}13821383function render_assignment_name() {1384const num_items = assignment.get("listing")?.size ?? 0;1385return (1386<span>1387{trunc_middle(assignment.get("path"), 80)}1388{assignment.get("deleted") ? <b> (deleted)</b> : undefined}1389{num_items == 0 ? " - add content to this assignment..." : undefined}1390</span>1391);1392}13931394function render_assignment_title_link() {1395return (1396<a1397href=""1398onClick={(e) => {1399e.preventDefault();1400actions.toggle_item_expansion(1401"assignment",1402assignment.get("assignment_id"),1403);1404}}1405>1406<Icon1407style={{ marginRight: "10px" }}1408name={is_expanded ? "caret-down" : "caret-right"}1409/>1410{render_assignment_name()}1411</a>1412);1413}14141415function render_summary_line() {1416return (1417<Row key="summary" style={{ backgroundColor: background }}>1418<Col md={12}>1419<h5>{render_assignment_title_link()}</h5>1420</Col>1421<Col md={12}>{render_summary_due_date()}</Col>1422</Row>1423);1424}14251426return (1427<div>1428<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>1429<Col xs={24}>1430{render_summary_line()}1431{is_expanded ? render_more() : undefined}1432</Col>1433</Row>1434</div>1435);1436}143714381439