Path: blob/master/src/packages/frontend/course/assignments/assignment.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 { ReactElement, useState } from "react";7import { DebounceInput } from "react-debounce-input";8import { FormattedMessage, useIntl } from "react-intl";9import { AppRedux, useActions } from "@cocalc/frontend/app-framework";10import {11DateTimePicker,12Icon,13IconName,14Loading,15MarkdownInput,16Tip,17} from "@cocalc/frontend/components";18import { course, labels } from "@cocalc/frontend/i18n";19import { capitalize, trunc_middle } from "@cocalc/util/misc";20import { CourseActions } from "../actions";21import { BigTime, Progress } from "../common";22import { STEP_NAMES, STEPS_INTL } from "../common/consts";23import { NbgraderButton } from "../nbgrader/nbgrader-button";24import type {25AssignmentRecord,26CourseStore,27IsGradingMap,28NBgraderRunInfo,29} from "../store";30import * as styles from "../styles";31import { AssignmentCopyStep, AssignmentStatus } from "../types";32import {33step_direction,34step_ready,35step_verb,36STEPS,37useButtonSize,38} from "../util";39import { StudentListForAssignment } from "./assignment-student-list";40import { ConfigurePeerGrading } from "./configure-peer";41import { STUDENT_SUBDIR } from "./consts";42import { SkipCopy } from "./skip";43import { ComputeServerButton } from "../compute";4445interface AssignmentProps {46active_feedback_edits: IsGradingMap;47assignment: AssignmentRecord;48background?: string;49expand_peer_config?: boolean;50frame_id?: string;51is_expanded?: boolean;52name: string;53nbgrader_run_info?: NBgraderRunInfo;54project_id: string;55redux: AppRedux;56students: object;57user_map: object;58}5960function useCopyConfirmState() {61const [copy_confirm, set_copy_confirm] = useState<{62[state in AssignmentCopyStep]: boolean;63}>({64assignment: false,65collect: false,66peer_assignment: false,67peer_collect: false,68return_graded: false,69});7071// modify flags, don't replace this entirely72function set(state: AssignmentCopyStep, value: boolean): void {73set_copy_confirm((prev) => ({ ...prev, [state]: value }));74}7576return { copy_confirm, set };77}7879export function Assignment({80active_feedback_edits,81assignment,82background,83expand_peer_config,84frame_id,85is_expanded,86name,87nbgrader_run_info,88project_id,89redux,90students,91user_map,92}: AssignmentProps) {93const intl = useIntl();94const size = useButtonSize();9596const [97copy_assignment_confirm_overwrite,98set_copy_assignment_confirm_overwrite,99] = useState<boolean>(false);100const [101copy_assignment_confirm_overwrite_text,102set_copy_assignment_confirm_overwrite_text,103] = useState<string>("");104const [student_search, set_student_search] = useState<string>("");105const [copy_confirm, set_copy_confirm] = useState<boolean>(false);106107const { copy_confirm: copy_confirm_state, set: set_copy_confirm_state } =108useCopyConfirmState();109const { copy_confirm: copy_confirm_all, set: set_copy_confirm_all } =110useCopyConfirmState();111112const actions = useActions<CourseActions>({ name });113114function get_store(): CourseStore {115return actions.get_store();116}117118function is_peer_graded() {119return !!assignment.getIn(["peer_grade", "enabled"]);120}121122function render_due() {123return (124<Space>125<div style={{ marginTop: "8px", color: "#666" }}>126<Tip127placement="top"128title="Set the due date"129tip="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. {intl.formatMessage(labels.you)} should also tell students when assignments are due (e.g., at the top of the assignment)."130>131Due132</Tip>133</div>134<DateTimePicker135placeholder={"Set Due Date"}136value={assignment.get("due_date")}137onChange={date_change}138/>139</Space>140);141}142143function date_change(date): void {144actions.assignments.set_due_date(145assignment.get("assignment_id"),146date != null ? date.toISOString() : undefined,147);148}149150function render_note() {151return (152<Row key="note" style={styles.note}>153<Col xs={4}>154<Tip155title="Notes about this assignment"156tip="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."157>158Private Assignment Notes159<br />160<span style={{ color: "#666" }} />161</Tip>162</Col>163<Col xs={20}>164<MarkdownInput165persist_id={166assignment.get("path") + assignment.get("assignment_id") + "note"167}168attach_to={name}169rows={6}170placeholder="Private notes about this assignment (not visible to students)"171default_value={assignment.get("note")}172on_save={(value) =>173actions.assignments.set_assignment_note(174assignment.get("assignment_id"),175value,176)177}178/>179</Col>180</Row>181);182}183184function render_export_file_use_times() {185return (186<Row key="file-use-times-export-used">187<Col xs={4}>188<Tip189title="Export when students used files"190tip="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."191>192Export file use times193<br />194<span style={{ color: "#666" }} />195</Tip>196</Col>197<Col xs={20}>198<Button199onClick={() =>200actions.export.file_use_times(assignment.get("assignment_id"))201}202>203Export file use times for this assignment204</Button>205</Col>206</Row>207);208}209210function render_export_assignment() {211return (212<Row key="file-use-times-export-collected">213<Col xs={4}>214<Tip215title="Export collected student files"216tip="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."217>218Export collected student files219<br />220<span style={{ color: "#666" }} />221</Tip>222</Col>223<Col xs={20}>224<Button225onClick={() =>226actions.assignments.export_collected(227assignment.get("assignment_id"),228)229}230>231Export collected student files to single directory, converting232Jupyter notebooks to pdf and html for easy offline grading.233</Button>234</Col>235</Row>236);237}238239function render_no_content() {240if (assignment.get("deleted")) {241// no point242return null;243}244return (245<div style={{ margin: "15px auto", maxWidth: "800px", fontSize: "12pt" }}>246There are no files in this assignment yet. Please{" "}247<a onClick={open_assignment_path}>open the directory</a> for this248assignment, then create, upload, or copy any content you want into that249directory. {intl.formatMessage(labels.you)} will then be able to send it250to all of your students.251</div>252);253}254255function render_more_header(num_files: number) {256let width;257const status: AssignmentStatus | undefined =258get_store().get_assignment_status(assignment.get("assignment_id"));259if (status == null) {260return <Loading key="loading_more" />;261}262const v: ReactElement<any>[] = [];263264const bottom = {265borderBottom: "1px solid grey",266paddingBottom: "15px",267marginBottom: "15px",268};269v.push(270<Row key="header3" style={{ ...bottom, marginTop: "15px" }}>271<Col md={4}>{render_open_button()}</Col>272<Col md={20}>273<Row>274<Col md={8} style={{ fontSize: "14px" }} key="due">275{render_due()}276</Col>277<Col md={16} key="delete">278<Row>279<Col md={10}>{render_peer_button()}</Col>280<Col md={14}>281<ComputeServerButton282actions={actions}283unit={assignment as any}284/>285<span className="pull-right">{render_delete_button()}</span>286</Col>287</Row>288</Col>289</Row>290</Col>291</Row>,292);293294if (expand_peer_config) {295v.push(296<Row key="header2-peer" style={bottom}>297<Col md={20} offset={4}>298{render_configure_peer()}299</Col>300</Row>,301);302}303304const peer = is_peer_graded();305if (peer) {306width = 4;307} else {308width = 6;309}310311if (num_files > 0) {312const buttons: ReactElement<any>[] = [];313const insert_grade_button = (key: string) => {314const b2 = render_skip_grading_button(status);315return buttons.push(316<Col md={width} key={key}>317{render_nbgrader_button(status)}318{b2}319</Col>,320);321};322323for (const name of STEPS(peer)) {324const b = render_button(name, status);325// squeeze in the skip grading button (don't add it to STEPS!)326if (!peer && name === "return_graded") {327insert_grade_button("skip_grading");328}329if (b != null) {330buttons.push(331<Col md={width} key={name}>332{b}333</Col>,334);335if (peer && name === "peer_collect") {336insert_grade_button("skip_peer_collect");337}338}339}340341v.push(342<Row key="header-control">343<Col md={4} key="search" style={{ paddingRight: "15px" }}>344<DebounceInput345debounceTimeout={500}346element={Input as any}347placeholder={"Filter students..."}348value={student_search}349onChange={(e) => set_student_search(e.target.value)}350/>351</Col>352<Col md={20} key="buttons">353<Row>{buttons}</Row>354</Col>355</Row>,356);357358v.push(359<Row key="header2-copy">360<Col md={20} offset={4}>361{render_copy_confirms(status)}362</Col>363</Row>,364);365}366/* The whiteSpace:'normal' here is because we put this in an367antd Card title, which has line wrapping disabled. */368return <div style={{ whiteSpace: "normal" }}>{v}</div>;369}370371function render_more() {372const num_files = assignment.get("listing")?.size ?? 0;373let body;374if (num_files == 0) {375body = render_no_content();376} else {377body = (378<>379<StudentListForAssignment380redux={redux}381frame_id={frame_id}382name={name}383assignment={assignment}384students={students}385user_map={user_map}386active_feedback_edits={active_feedback_edits}387nbgrader_run_info={nbgrader_run_info}388search={student_search}389/>390{render_note()}391<br />392<hr />393<br />394{render_export_file_use_times()}395<br />396{render_export_assignment()}397</>398);399}400return (401<Row key="more">402<Col sm={24}>403<Card title={render_more_header(num_files)}> {body}</Card>404</Col>405</Row>406);407}408409function open_assignment_path(): void {410if (assignment.get("listing")?.size == 0) {411// there are no files yet, so we *close* the assignment412// details panel. This is just **a hack** so that the user413// has to re-open it after adding files, which will trigger414// updating the directory listing, hence show the rest415// of the assignment info. The alternative would be416// polling the directory or watching listings, which is417// a lot more work to properly implement.418actions.toggle_item_expansion(419"assignment",420assignment.get("assignment_id"),421);422}423return redux424.getProjectActions(project_id)425.open_directory(assignment.get("path"));426}427428function render_open_button() {429return (430<Tip431key="open"432title={433<span>434<Icon name="folder-open" /> Open Folder435</span>436}437tip="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."438>439<Button onClick={open_assignment_path}>440<Icon name="folder-open" /> {intl.formatMessage(labels.open)}...441</Button>442</Tip>443);444}445446function show_copy_confirm(): void {447set_copy_confirm_state("assignment", true);448set_copy_confirm(true);449const assignment_id: string | undefined = assignment.get("assignment_id");450actions.assignments.update_listing(assignment_id);451}452453function render_assignment_button(status) {454const last_assignment = assignment.get("last_assignment");455// Primary if it hasn't been assigned before or if it hasn't started assigning.456let type;457if (458!last_assignment ||459!(last_assignment.get("time") || last_assignment.get("start"))460) {461type = "primary";462} else {463type = "default";464}465if (status.assignment > 0 && status.not_assignment === 0) {466type = "dashed";467}468469const label = intl.formatMessage(STEPS_INTL, {470step: STEP_NAMES.indexOf("Assign"),471});472const you = intl.formatMessage(labels.you);473const students = intl.formatMessage(course.students);474const tooltip = intl.formatMessage({475id: "course.assignments.assign.tooltip",476defaultMessage:477"Copy the files for this assignment from this project to all other student projects.",478description: "Students in an online course",479});480481return [482<Button483key="assign"484type={type}485size={size}486onClick={show_copy_confirm}487disabled={copy_confirm}488>489<Tip490title={491<span>492{label}: <Icon name="user-secret" /> {you}{" "}493<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}494</span>495}496tip={tooltip}497>498<Icon name="share-square" /> {label}...499</Tip>500</Button>,501<Progress502key="progress"503done={status.assignment}504not_done={status.not_assignment}505step="assigned"506skipped={assignment.get("skip_assignment")}507/>,508];509}510511function render_copy_confirms(status) {512const steps = STEPS(is_peer_graded());513const result: (ReactElement<any> | undefined)[] = [];514for (const step of steps) {515if (copy_confirm_state[step]) {516result.push(render_copy_confirm(step, status));517} else {518result.push(undefined);519}520}521return result;522}523524function render_copy_confirm(step, status) {525return (526<span key={`copy_confirm_${step}`}>527{status[step] === 0528? render_copy_confirm_to_all(step, status)529: undefined}530{status[step] !== 0531? render_copy_confirm_to_all_or_new(step, status)532: undefined}533</span>534);535}536537function render_copy_cancel(step) {538const cancel = () => {539set_copy_confirm_state(step, false);540set_copy_confirm_all(step, false);541set_copy_confirm(false);542set_copy_assignment_confirm_overwrite(false);543};544return (545<Button key="cancel" onClick={cancel} size={size}>546{intl.formatMessage(labels.close)}547</Button>548);549}550551function render_copy_assignment_confirm_overwrite(step) {552if (!copy_assignment_confirm_overwrite) {553return;554}555const do_it = () => {556copy_assignment(step, false, true);557set_copy_assignment_confirm_overwrite(false);558set_copy_assignment_confirm_overwrite_text("");559};560return (561<div style={{ marginTop: "15px" }}>562Type in "OVERWRITE" if you are sure you want to overwrite any work they563may have.564<Input565autoFocus566onChange={(e) =>567set_copy_assignment_confirm_overwrite_text((e.target as any).value)568}569style={{ marginTop: "1ex" }}570/>571<Space style={{ textAlign: "center", marginTop: "15px" }}>572{render_copy_cancel(step)}573<Button574disabled={copy_assignment_confirm_overwrite_text !== "OVERWRITE"}575danger576onClick={do_it}577>578<Icon name="exclamation-triangle" /> Confirm replacing files579</Button>580</Space>581</div>582);583}584585function copy_assignment(586step,587new_only: boolean,588overwrite: boolean = false,589) {590// assign assignment to all (non-deleted) students591const assignment_id: string | undefined = assignment.get("assignment_id");592if (assignment_id == null) throw Error("bug");593switch (step) {594case "assignment":595actions.assignments.copy_assignment_to_all_students(596assignment_id,597new_only,598overwrite,599);600break;601case "collect":602actions.assignments.copy_assignment_from_all_students(603assignment_id,604new_only,605);606break;607case "peer_assignment":608actions.assignments.peer_copy_to_all_students(assignment_id, new_only);609break;610case "peer_collect":611actions.assignments.peer_collect_from_all_students(612assignment_id,613new_only,614);615break;616case "return_graded":617actions.assignments.return_assignment_to_all_students(618assignment_id,619new_only,620);621break;622default:623console.log(`BUG -- unknown step: ${step}`);624}625set_copy_confirm_state(step, false);626set_copy_confirm_all(step, false);627set_copy_confirm(false);628}629630function render_skip(step: AssignmentCopyStep) {631if (step === "return_graded") {632return;633}634return (635<div style={{ float: "right" }}>636<SkipCopy assignment={assignment} step={step} actions={actions} />637</div>638);639}640641function render_has_student_subdir(step: AssignmentCopyStep) {642if (step != "assignment" || !assignment.get("has_student_subdir")) return;643return (644<Alert645style={{ marginBottom: "15px" }}646type="info"647message={`NOTE: Only the ${STUDENT_SUBDIR}/ subdirectory will be copied to the students.`}648/>649);650}651652function render_parallel() {653const n = get_store().get_copy_parallel();654return (655<Tip656title={`Parallel limit: copy ${n} assignments at a time`}657tip="This is the max number of assignments to copy in parallel. Change this in course configuration."658>659<div style={{ marginTop: "10px", fontWeight: 400 }}>660Copy up to {n} assignments at once.661</div>662</Tip>663);664}665666function render_copy_confirm_to_all(step: AssignmentCopyStep, status) {667const n = status[`not_${step}`];668const message = (669<div>670<div style={{ marginBottom: "15px" }}>671{capitalize(step_verb(step))} this homework {step_direction(step)} the{" "}672{n} student{n > 1 ? "s" : ""}673{step_ready(step, n)}?674</div>675{render_has_student_subdir(step)}676{render_skip(step)}677<Space wrap>678{render_copy_cancel(step)}679<Button680key="yes"681type="primary"682onClick={() => copy_assignment(step, false)}683>684Yes685</Button>686</Space>687{render_parallel()}688</div>689);690return (691<Alert692type="warning"693key={`${step}_confirm_to_all`}694style={{ marginTop: "15px" }}695message={message}696/>697);698}699700function copy_confirm_all_caution(step: AssignmentCopyStep) {701switch (step) {702case "assignment":703return (704<span>705This will recopy all of the files to them. CAUTION: if you update a706file that a student has also worked on, their work will get copied707to a backup file ending in a tilde, or possibly only be available in708snapshots. Select "Replace student files!" in case you do <b>not</b>{" "}709want to create any backups and also <b>delete</b> all other files in710the assignment folder of their projects.{" "}711<a712target="_blank"713href="https://github.com/sagemathinc/cocalc/wiki/CourseCopy"714>715(more details)716</a>717.718</span>719);720case "collect":721return "This will recollect all of the homework from them. CAUTION: if you have graded/edited a file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.";722case "return_graded":723return "This will rereturn all of the graded files to them.";724case "peer_assignment":725return "This will recopy all of the files to them. CAUTION: if there is a file a student has also worked on grading, their work will get copied to a backup file ending in a tilde, or possibly be only available in snapshots.";726case "peer_collect":727return "This will recollect all of the peer-graded homework from the students. CAUTION: if you have graded/edited a previously collected file that a student has updated, your work will get copied to a backup file ending in a tilde, or possibly only be available in snapshots.";728}729}730731function render_copy_confirm_overwrite_all(step: AssignmentCopyStep) {732return (733<div key={"copy_confirm_overwrite_all"} style={{ marginTop: "15px" }}>734<div style={{ marginBottom: "15px" }}>735{copy_confirm_all_caution(step)}736</div>737<Space wrap>738{render_copy_cancel(step)}739<Button740key={"all"}741type={"dashed"}742disabled={copy_assignment_confirm_overwrite}743onClick={() => copy_assignment(step, false)}744>745Yes, do it (with backup)746</Button>747{step === "assignment" ? (748<Button749key={"all-overwrite"}750type={"dashed"}751onClick={() => set_copy_assignment_confirm_overwrite(true)}752disabled={copy_assignment_confirm_overwrite}753>754Replace student files!755</Button>756) : undefined}757</Space>758{render_copy_assignment_confirm_overwrite(step)}759</div>760);761}762763function render_copy_confirm_to_all_or_new(step: AssignmentCopyStep, status) {764const n = status[`not_${step}`];765const m = n + status[step];766const message = (767<div>768<div style={{ marginBottom: "15px" }}>769{capitalize(step_verb(step))} this homework {step_direction(step)}770...771</div>772{render_has_student_subdir(step)}773{render_skip(step)}774<Space wrap>775{render_copy_cancel(step)}776<Button777key="all"778danger779onClick={() => {780set_copy_confirm_all(step, true);781set_copy_confirm(true);782}}783disabled={copy_confirm_all[step]}784>785{step === "assignment" ? "All" : "The"} {m} students786{step_ready(step, m)}...787</Button>788{n ? (789<Button790key="new"791type="primary"792onClick={() => copy_assignment(step, true)}793>794The {n} student{n > 1 ? "s" : ""} not already {step_verb(step)}795ed {step_direction(step)}796</Button>797) : undefined}798</Space>799{copy_confirm_all[step]800? render_copy_confirm_overwrite_all(step)801: undefined}802{render_parallel()}803</div>804);805return (806<Alert807type="warning"808key={`${step}_confirm_to_all_or_new`}809style={{ marginTop: "15px" }}810message={message}811/>812);813}814815function render_collect_tip() {816return (817<span key="normal">818<FormattedMessage819id="course.assignments.collect.tooltip"820defaultMessage={`Collect an assignment from all of your students.821(There is currently no way to schedule collection at a specific time;822instead, collection happens when you click the button.)`}823/>824</span>825);826}827828function render_button(state: AssignmentCopyStep, status) {829switch (state) {830case "collect":831return render_collect_button(status);832case "return_graded":833return render_return_graded_button(status);834case "peer_assignment":835return render_peer_assignment_button(status);836case "peer_collect":837return render_peer_collect_button(status);838case "assignment":839return render_assignment_button(status);840}841}842843function render_collect_button(status) {844if (status.assignment === 0) {845// no button if nothing ever assigned846return;847}848let type;849if (status.collect > 0) {850// Have already collected something851if (status.not_collect === 0) {852type = "dashed";853} else {854type = "default";855}856} else {857type = "primary";858}859return [860<Button861key="collect"862onClick={() => {863set_copy_confirm_state("collect", true);864set_copy_confirm(true);865}}866disabled={copy_confirm}867type={type}868size={size}869>870<Tip871title={872<span>873Collect: <Icon name="users" />{" "}874{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}875<Icon name="user-secret" /> You876</span>877}878tip={render_collect_tip()}879>880<Icon name="share-square" rotate={"180"} />{" "}881{intl.formatMessage(STEPS_INTL, {882step: STEP_NAMES.indexOf("Collect"),883})}884...885</Tip>886</Button>,887<Progress888key="progress"889done={status.collect}890not_done={status.not_collect}891step="collected"892skipped={assignment.get("skip_collect")}893/>,894];895}896897function render_peer_assign_tip() {898return (899<span key="normal">900Send copies of collected homework out to all students for peer grading.901</span>902);903}904905function render_peer_assignment_button(status) {906// Render the "Peer Assign..." button in the top row, for peer assigning to all907// students in the course.908if (status.peer_assignment == null) {909// not peer graded910return;911}912if (status.not_collect + status.not_assignment > 0) {913// collect everything before peer grading914return;915}916if (status.collect === 0) {917// nothing to peer assign918return;919}920let type;921if (status.peer_assignment > 0) {922// haven't peer-assigned anything yet923if (status.not_peer_assignment === 0) {924type = "dashed";925} else {926type = "default";927}928} else {929type = "primary";930}931const label = intl.formatMessage(STEPS_INTL, {932step: STEP_NAMES.indexOf("Peer Assign"),933});934return [935<Button936key="peer-assign"937onClick={() => {938set_copy_confirm_state("peer_assignment", true);939set_copy_confirm(true);940}}941disabled={copy_confirm}942type={type}943size={size}944>945<Tip946title={947<span>948{label}: <Icon name="users" /> {intl.formatMessage(labels.you)}{" "}949<Icon name="arrow-right" /> <Icon name="user-secret" />{" "}950{intl.formatMessage(course.students)}951</span>952}953tip={render_peer_assign_tip()}954>955<Icon name="share-square" /> {label}...956</Tip>957</Button>,958<Progress959key="progress"960done={status.peer_assignment}961not_done={status.not_peer_assignment}962step="peer assigned"963/>,964];965}966967function render_peer_collect_tip() {968return (969<span key="normal">Collect the peer grading that your students did.</span>970);971}972973function render_peer_collect_button(status) {974// Render the "Peer Collect..." button in the top row, for collecting peer grading from all975// students in the course.976if (status.peer_collect == null) {977return;978}979if (status.peer_assignment === 0) {980// haven't even peer assigned anything -- so nothing to collect981return;982}983if (status.not_peer_assignment > 0) {984// everybody must have received peer assignment, or collecting isn't allowed985return;986}987let type;988if (status.peer_collect > 0) {989// haven't peer-collected anything yet990if (status.not_peer_collect === 0) {991type = "dashed";992} else {993type = "default";994}995} else {996// warning, since we have already collected and this may overwrite997type = "primary";998}999const label = intl.formatMessage(STEPS_INTL, {1000step: STEP_NAMES.indexOf("Peer Collect"),1001});1002return [1003<Button1004key="peer-collect"1005onClick={() => {1006set_copy_confirm_state("peer_collect", true);1007set_copy_confirm(true);1008}}1009disabled={copy_confirm}1010type={type}1011size={size}1012>1013<Tip1014title={1015<span>1016{label}: <Icon name="users" />{" "}1017{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}1018<Icon name="user-secret" /> You1019</span>1020}1021tip={render_peer_collect_tip()}1022>1023<Icon name="share-square" rotate="180" /> {label}...1024</Tip>1025</Button>,1026<Progress1027key="progress"1028done={status.peer_collect}1029not_done={status.not_peer_collect}1030step="peer collected"1031/>,1032];1033}10341035function toggle_skip_grading() {1036actions.assignments.set_skip(1037assignment.get("assignment_id"),1038"grading",1039!assignment.get("skip_grading"),1040);1041}10421043function render_skip_grading_button(status) {1044if (status.collect === 0) {1045// No button if nothing collected.1046return;1047}1048const icon: IconName = assignment.get("skip_grading")1049? "check-square-o"1050: "square-o";1051return (1052<Button onClick={toggle_skip_grading} size={size}>1053<Icon name={icon} /> Skip entering grades1054</Button>1055);1056}10571058function render_nbgrader_button(status) {1059if (1060status.collect === 0 ||1061!assignment.get("nbgrader") ||1062assignment.get("skip_grading")1063) {1064// No button if nothing collected or not nbgrader support or1065// decided to skip grading1066return;1067}10681069return (1070<NbgraderButton1071assignment_id={assignment.get("assignment_id")}1072name={name}1073/>1074);1075}10761077function render_return_graded_button(status) {1078if (status.collect === 0) {1079// No button if nothing collected.1080return;1081}1082if (status.peer_collect != null && status.peer_collect === 0) {1083// Peer grading enabled, but we didn't collect anything yet1084return;1085}1086if (1087!assignment.get("skip_grading") &&1088status.not_return_graded === 0 &&1089status.return_graded === 01090) {1091// Nothing unreturned and ungraded yet and also nothing returned yet1092return;1093}1094let type;1095if (status.return_graded > 0) {1096// Have already returned some1097if (status.not_return_graded === 0) {1098type = "dashed";1099} else {1100type = "default";1101}1102} else {1103type = "primary";1104}1105const label = intl.formatMessage(STEPS_INTL, {1106step: STEP_NAMES.indexOf("Return"),1107});1108return [1109<Button1110key="return"1111onClick={() => {1112set_copy_confirm_state("return_graded", true);1113set_copy_confirm(true);1114}}1115disabled={copy_confirm}1116type={type}1117size={size}1118>1119<Tip1120title={1121<span>1122{label}: <Icon name="user-secret" /> You{" "}1123<Icon name="arrow-right" /> <Icon name="users" />{" "}1124{intl.formatMessage(course.students)}{" "}1125</span>1126}1127tip="Copy the graded versions of files for this assignment from this project to all other student projects."1128>1129<Icon name="share-square" /> {label}...1130</Tip>1131</Button>,1132<Progress1133key="progress"1134done={status.return_graded}1135not_done={status.not_return_graded}1136step="returned"1137/>,1138];1139}11401141function delete_assignment() {1142actions.assignments.delete_assignment(assignment.get("assignment_id"));1143}11441145function undelete_assignment() {1146return actions.assignments.undelete_assignment(1147assignment.get("assignment_id"),1148);1149}11501151function render_delete_button() {1152if (assignment.get("deleted")) {1153return (1154<Tip1155key="delete"1156placement="left"1157title={intl.formatMessage({1158id: "course.assignment.undelete.title",1159defaultMessage: "Undelete assignment",1160})}1161tip={intl.formatMessage({1162id: "course.assignment.undelete.tooltip",1163defaultMessage:1164"Make the assignment visible again in the assignment list and in student grade lists.",1165})}1166>1167<Button onClick={undelete_assignment}>1168<Icon name="trash" /> {intl.formatMessage(labels.undelete)}1169</Button>1170</Tip>1171);1172} else {1173return (1174<Popconfirm1175title={1176<div style={{ maxWidth: "400px" }}>1177<FormattedMessage1178id="course.assignment.delete.confirm.info"1179defaultMessage={`<b>Are you sure you want to delete {name}"?</b>1180{br}1181This removes it from the assignment list and student grade lists,1182but does not delete any files off of disk.1183You can undelete an assignment later by showing it using the 'Show deleted assignments' button.`}1184values={{1185name: trunc_middle(assignment.get("path"), 24),1186br: <br />,1187}}1188/>1189</div>1190}1191onConfirm={delete_assignment}1192cancelText={intl.formatMessage(labels.cancel)}1193>1194<Button size={size}>1195<Icon name="trash" /> {intl.formatMessage(labels.delete)}...1196</Button>1197</Popconfirm>1198);1199}1200}12011202function render_configure_peer() {1203return <ConfigurePeerGrading actions={actions} assignment={assignment} />;1204}12051206function render_peer_button() {1207let icon;1208if (is_peer_graded()) {1209icon = "check-square-o";1210} else {1211icon = "square-o";1212}1213return (1214<Button1215disabled={expand_peer_config}1216onClick={() =>1217actions.toggle_item_expansion(1218"peer_config",1219assignment.get("assignment_id"),1220)1221}1222>1223<Icon name={icon} /> Peer Grading...1224</Button>1225);1226}12271228function render_summary_due_date() {1229const due_date = assignment.get("due_date");1230if (due_date) {1231return (1232<div style={{ marginTop: "12px" }}>1233Due <BigTime date={due_date} />1234</div>1235);1236}1237}12381239function render_assignment_name() {1240const num_items = assignment.get("listing")?.size ?? 0;1241return (1242<span>1243{trunc_middle(assignment.get("path"), 80)}1244{assignment.get("deleted") ? <b> (deleted)</b> : undefined}1245{num_items == 0 ? " - add content to this assignment..." : undefined}1246</span>1247);1248}12491250function render_assignment_title_link() {1251return (1252<a1253href=""1254onClick={(e) => {1255e.preventDefault();1256actions.toggle_item_expansion(1257"assignment",1258assignment.get("assignment_id"),1259);1260}}1261>1262<Icon1263style={{ marginRight: "10px" }}1264name={is_expanded ? "caret-down" : "caret-right"}1265/>1266{render_assignment_name()}1267</a>1268);1269}12701271function render_summary_line() {1272return (1273<Row key="summary" style={{ backgroundColor: background }}>1274<Col md={12}>1275<h5>{render_assignment_title_link()}</h5>1276</Col>1277<Col md={12}>{render_summary_due_date()}</Col>1278</Row>1279);1280}12811282return (1283<div>1284<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>1285<Col xs={24}>1286{render_summary_line()}1287{is_expanded ? render_more() : undefined}1288</Col>1289</Row>1290</div>1291);1292}129312941295