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/assignments/assignment.tsx
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { Alert, Button, Card, Col, Input, Popconfirm, Row, Space } from "antd";6import { ReactElement, useState } from "react";7import { DebounceInput } from "react-debounce-input";8import { FormattedMessage, useIntl } from "react-intl";910import { AppRedux, useActions } from "@cocalc/frontend/app-framework";11import {12DateTimePicker,13Icon,14IconName,15Loading,16MarkdownInput,17Tip,18} from "@cocalc/frontend/components";19import { course, labels } from "@cocalc/frontend/i18n";20import { capitalize, trunc_middle } from "@cocalc/util/misc";21import { CourseActions } from "../actions";22import { BigTime, Progress } from "../common";23import { STEP_NAMES, STEPS_INTL } from "../common/consts";24import { NbgraderButton } from "../nbgrader/nbgrader-button";25import {26AssignmentRecord,27CourseStore,28IsGradingMap,29NBgraderRunInfo,30SortDescription,31} from "../store";32import * as styles from "../styles";33import { AssignmentCopyStep, AssignmentStatus } from "../types";34import {35step_direction,36step_ready,37step_verb,38STEPS,39useButtonSize,40} from "../util";41import { StudentListForAssignment } from "./assignment-student-list";42import { ConfigurePeerGrading } from "./configure-peer";43import { STUDENT_SUBDIR } from "./consts";44import { SkipCopy } from "./skip";4546interface AssignmentProps {47active_feedback_edits: IsGradingMap;48active_student_sort: SortDescription;49assignment: AssignmentRecord;50background?: string;51expand_peer_config?: boolean;52frame_id?: string;53is_expanded?: boolean;54name: string;55nbgrader_run_info?: NBgraderRunInfo;56project_id: string;57redux: AppRedux;58students: object;59user_map: object;60}6162function useCopyConfirmState() {63const [copy_confirm, set_copy_confirm] = useState<{64[state in AssignmentCopyStep]: boolean;65}>({66assignment: false,67collect: false,68peer_assignment: false,69peer_collect: false,70return_graded: false,71});7273// modify flags, don't replace this entirely74function set(state: AssignmentCopyStep, value: boolean): void {75set_copy_confirm((prev) => ({ ...prev, [state]: value }));76}7778return { copy_confirm, set };79}8081export function Assignment({82active_feedback_edits,83active_student_sort,84assignment,85background,86expand_peer_config,87frame_id,88is_expanded,89name,90nbgrader_run_info,91project_id,92redux,93students,94user_map,95}: AssignmentProps) {96const intl = useIntl();97const size = useButtonSize();9899const [100copy_assignment_confirm_overwrite,101set_copy_assignment_confirm_overwrite,102] = useState<boolean>(false);103const [104copy_assignment_confirm_overwrite_text,105set_copy_assignment_confirm_overwrite_text,106] = useState<string>("");107const [student_search, set_student_search] = useState<string>("");108const [copy_confirm, set_copy_confirm] = useState<boolean>(false);109110const { copy_confirm: copy_confirm_state, set: set_copy_confirm_state } =111useCopyConfirmState();112const { copy_confirm: copy_confirm_all, set: set_copy_confirm_all } =113useCopyConfirmState();114115const actions = useActions<CourseActions>({ name });116117function get_store(): CourseStore {118return actions.get_store();119}120121function is_peer_graded() {122return !!assignment.getIn(["peer_grade", "enabled"]);123}124125function render_due() {126return (127<Row>128<Col xs={2} style={{ marginTop: "8px", color: "#666" }}>129<Tip130placement="top"131title="Set the due date"132tip="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)."133>134Due135</Tip>136</Col>137<Col xs={22}>138<DateTimePicker139placeholder={"Set Due Date"}140value={assignment.get("due_date")}141onChange={date_change}142/>143</Col>144</Row>145);146}147148function date_change(date): void {149actions.assignments.set_due_date(150assignment.get("assignment_id"),151date != null ? date.toISOString() : undefined,152);153}154155function render_note() {156return (157<Row key="note" style={styles.note}>158<Col xs={4}>159<Tip160title="Notes about this assignment"161tip="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."162>163Private Assignment Notes164<br />165<span style={{ color: "#666" }} />166</Tip>167</Col>168<Col xs={20}>169<MarkdownInput170persist_id={171assignment.get("path") + assignment.get("assignment_id") + "note"172}173attach_to={name}174rows={6}175placeholder="Private notes about this assignment (not visible to students)"176default_value={assignment.get("note")}177on_save={(value) =>178actions.assignments.set_assignment_note(179assignment.get("assignment_id"),180value,181)182}183/>184</Col>185</Row>186);187}188189function render_export_file_use_times() {190return (191<Row key="file-use-times-export-used">192<Col xs={4}>193<Tip194title="Export when students used files"195tip="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."196>197Export file use times198<br />199<span style={{ color: "#666" }} />200</Tip>201</Col>202<Col xs={20}>203<Button204onClick={() =>205actions.export.file_use_times(assignment.get("assignment_id"))206}207>208Export file use times for this assignment209</Button>210</Col>211</Row>212);213}214215function render_export_assignment() {216return (217<Row key="file-use-times-export-collected">218<Col xs={4}>219<Tip220title="Export collected student files"221tip="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."222>223Export collected student files224<br />225<span style={{ color: "#666" }} />226</Tip>227</Col>228<Col xs={20}>229<Button230onClick={() =>231actions.assignments.export_collected(232assignment.get("assignment_id"),233)234}235>236Export collected student files to single directory, converting237Jupyter notebooks to pdf and html for easy offline grading.238</Button>239</Col>240</Row>241);242}243244function render_no_content() {245if (assignment.get("deleted")) {246// no point247return null;248}249return (250<div style={{ margin: "15px auto", maxWidth: "800px", fontSize: "12pt" }}>251There are no files in this assignment yet. Please{" "}252<a onClick={open_assignment_path}>open the directory</a> for this253assignment, then create, upload, or copy any content you want into that254directory. {intl.formatMessage(labels.you)} will then be able to send it255to all of your students.256</div>257);258}259260function render_more_header(num_files: number) {261let width;262const status: AssignmentStatus | undefined =263get_store().get_assignment_status(assignment.get("assignment_id"));264if (status == null) {265return <Loading key="loading_more" />;266}267const v: ReactElement<any>[] = [];268269const bottom = {270borderBottom: "1px solid grey",271paddingBottom: "15px",272marginBottom: "15px",273};274v.push(275<Row key="header3" style={{ ...bottom, marginTop: "15px" }}>276<Col md={4}>{render_open_button()}</Col>277<Col md={20}>278<Row>279<Col md={12} style={{ fontSize: "14px" }} key="due">280{render_due()}281</Col>282<Col md={12} key="delete">283<Row>284<Col md={14}>{render_peer_button()}</Col>285<Col md={10}>286<span className="pull-right">{render_delete_button()}</span>287</Col>288</Row>289</Col>290</Row>291</Col>292</Row>,293);294295if (expand_peer_config) {296v.push(297<Row key="header2-peer" style={bottom}>298<Col md={20} offset={4}>299{render_configure_peer()}300</Col>301</Row>,302);303}304305const peer = is_peer_graded();306if (peer) {307width = 4;308} else {309width = 6;310}311312if (num_files > 0) {313const buttons: ReactElement<any>[] = [];314const insert_grade_button = (key: string) => {315const b2 = render_skip_grading_button(status);316return buttons.push(317<Col md={width} key={key}>318{render_nbgrader_button(status)}319{b2}320</Col>,321);322};323324for (const name of STEPS(peer)) {325const b = render_button(name, status);326// squeeze in the skip grading button (don't add it to STEPS!)327if (!peer && name === "return_graded") {328insert_grade_button("skip_grading");329}330if (b != null) {331buttons.push(332<Col md={width} key={name}>333{b}334</Col>,335);336if (peer && name === "peer_collect") {337insert_grade_button("skip_peer_collect");338}339}340}341342v.push(343<Row key="header-control">344<Col md={4} key="search" style={{ paddingRight: "15px" }}>345<DebounceInput346debounceTimeout={500}347element={Input as any}348placeholder={"Filter students..."}349value={student_search}350onChange={(e) => set_student_search(e.target.value)}351/>352</Col>353<Col md={20} key="buttons">354<Row>{buttons}</Row>355</Col>356</Row>,357);358359v.push(360<Row key="header2-copy">361<Col md={20} offset={4}>362{render_copy_confirms(status)}363</Col>364</Row>,365);366}367/* The whiteSpace:'normal' here is because we put this in an368antd Card title, which has line wrapping disabled. */369return <div style={{ whiteSpace: "normal" }}>{v}</div>;370}371372function render_more() {373const num_files = assignment.get("listing")?.size ?? 0;374let body;375if (num_files == 0) {376body = render_no_content();377} else {378body = (379<>380<StudentListForAssignment381redux={redux}382frame_id={frame_id}383name={name}384assignment={assignment}385students={students}386user_map={user_map}387active_student_sort={active_student_sort}388active_feedback_edits={active_feedback_edits}389nbgrader_run_info={nbgrader_run_info}390search={student_search}391/>392{render_note()}393<br />394<hr />395<br />396{render_export_file_use_times()}397<br />398{render_export_assignment()}399</>400);401}402return (403<Row key="more">404<Col sm={24}>405<Card title={render_more_header(num_files)}> {body}</Card>406</Col>407</Row>408);409}410411function open_assignment_path(): void {412if (assignment.get("listing")?.size == 0) {413// there are no files yet, so we *close* the assignment414// details panel. This is just **a hack** so that the user415// has to re-open it after adding files, which will trigger416// updating the directory listing, hence show the rest417// of the assignment info. The alternative would be418// polling the directory or watching listings, which is419// a lot more work to properly implement.420actions.toggle_item_expansion(421"assignment",422assignment.get("assignment_id"),423);424}425return redux426.getProjectActions(project_id)427.open_directory(assignment.get("path"));428}429430function render_open_button() {431return (432<Tip433key="open"434title={435<span>436<Icon name="folder-open" /> Open Folder437</span>438}439tip="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."440>441<Button onClick={open_assignment_path}>442<Icon name="folder-open" /> {intl.formatMessage(labels.open)}...443</Button>444</Tip>445);446}447448function show_copy_confirm(): void {449set_copy_confirm_state("assignment", true);450set_copy_confirm(true);451const assignment_id: string | undefined = assignment.get("assignment_id");452actions.assignments.update_listing(assignment_id);453}454455function render_assignment_button(status) {456const last_assignment = assignment.get("last_assignment");457// Primary if it hasn't been assigned before or if it hasn't started assigning.458let type;459if (460!last_assignment ||461!(last_assignment.get("time") || last_assignment.get("start"))462) {463type = "primary";464} else {465type = "default";466}467if (status.assignment > 0 && status.not_assignment === 0) {468type = "dashed";469}470471const label = intl.formatMessage(STEPS_INTL, {472step: STEP_NAMES.indexOf("Assign"),473});474const you = intl.formatMessage(labels.you);475const students = intl.formatMessage(course.students);476const tooltip = intl.formatMessage({477id: "course.assignments.assign.tooltip",478defaultMessage:479"Copy the files for this assignment from this project to all other student projects.",480description: "Students in an online course",481});482483return [484<Button485key="assign"486type={type}487size={size}488onClick={show_copy_confirm}489disabled={copy_confirm}490>491<Tip492title={493<span>494{label}: <Icon name="user-secret" /> {you}{" "}495<Icon name="arrow-right" /> <Icon name="users" /> {students}{" "}496</span>497}498tip={tooltip}499>500<Icon name="share-square" /> {label}...501</Tip>502</Button>,503<Progress504key="progress"505done={status.assignment}506not_done={status.not_assignment}507step="assigned"508skipped={assignment.get("skip_assignment")}509/>,510];511}512513function render_copy_confirms(status) {514const steps = STEPS(is_peer_graded());515const result: (ReactElement<any> | undefined)[] = [];516for (const step of steps) {517if (copy_confirm_state[step]) {518result.push(render_copy_confirm(step, status));519} else {520result.push(undefined);521}522}523return result;524}525526function render_copy_confirm(step, status) {527return (528<span key={`copy_confirm_${step}`}>529{status[step] === 0530? render_copy_confirm_to_all(step, status)531: undefined}532{status[step] !== 0533? render_copy_confirm_to_all_or_new(step, status)534: undefined}535</span>536);537}538539function render_copy_cancel(step) {540const cancel = () => {541set_copy_confirm_state(step, false);542set_copy_confirm_all(step, false);543set_copy_confirm(false);544set_copy_assignment_confirm_overwrite(false);545};546return (547<Button key="cancel" onClick={cancel} size={size}>548{intl.formatMessage(labels.close)}549</Button>550);551}552553function render_copy_assignment_confirm_overwrite(step) {554if (!copy_assignment_confirm_overwrite) {555return;556}557const do_it = () => {558copy_assignment(step, false, true);559set_copy_assignment_confirm_overwrite(false);560set_copy_assignment_confirm_overwrite_text("");561};562return (563<div style={{ marginTop: "15px" }}>564Type in "OVERWRITE" if you are sure you want to overwrite any work they565may have.566<Input567autoFocus568onChange={(e) =>569set_copy_assignment_confirm_overwrite_text((e.target as any).value)570}571style={{ marginTop: "1ex" }}572/>573<Space style={{ textAlign: "center", marginTop: "15px" }}>574{render_copy_cancel(step)}575<Button576disabled={copy_assignment_confirm_overwrite_text !== "OVERWRITE"}577danger578onClick={do_it}579>580<Icon name="exclamation-triangle" /> Confirm replacing files581</Button>582</Space>583</div>584);585}586587function copy_assignment(588step,589new_only: boolean,590overwrite: boolean = false,591) {592// assign assignment to all (non-deleted) students593const assignment_id: string | undefined = assignment.get("assignment_id");594if (assignment_id == null) throw Error("bug");595switch (step) {596case "assignment":597actions.assignments.copy_assignment_to_all_students(598assignment_id,599new_only,600overwrite,601);602break;603case "collect":604actions.assignments.copy_assignment_from_all_students(605assignment_id,606new_only,607);608break;609case "peer_assignment":610actions.assignments.peer_copy_to_all_students(assignment_id, new_only);611break;612case "peer_collect":613actions.assignments.peer_collect_from_all_students(614assignment_id,615new_only,616);617break;618case "return_graded":619actions.assignments.return_assignment_to_all_students(620assignment_id,621new_only,622);623break;624default:625console.log(`BUG -- unknown step: ${step}`);626}627set_copy_confirm_state(step, false);628set_copy_confirm_all(step, false);629set_copy_confirm(false);630}631632function render_skip(step: AssignmentCopyStep) {633if (step === "return_graded") {634return;635}636return (637<div style={{ float: "right" }}>638<SkipCopy assignment={assignment} step={step} actions={actions} />639</div>640);641}642643function render_has_student_subdir(step: AssignmentCopyStep) {644if (step != "assignment" || !assignment.get("has_student_subdir")) return;645return (646<Alert647style={{ marginBottom: "15px" }}648type="info"649message={`NOTE: Only the ${STUDENT_SUBDIR}/ subdirectory will be copied to the students.`}650/>651);652}653654function render_parallel() {655const n = get_store().get_copy_parallel();656return (657<Tip658title={`Parallel limit: copy ${n} assignments at a time`}659tip="This is the max number of assignments to copy in parallel. Change this in course configuration."660>661<div style={{ marginTop: "10px", fontWeight: 400 }}>662Copy up to {n} assignments at once.663</div>664</Tip>665);666}667668function render_copy_confirm_to_all(step: AssignmentCopyStep, status) {669const n = status[`not_${step}`];670const message = (671<div>672<div style={{ marginBottom: "15px" }}>673{capitalize(step_verb(step))} this homework {step_direction(step)} the{" "}674{n} student{n > 1 ? "s" : ""}675{step_ready(step, n)}?676</div>677{render_has_student_subdir(step)}678{render_skip(step)}679<Space wrap>680{render_copy_cancel(step)}681<Button682key="yes"683type="primary"684onClick={() => copy_assignment(step, false)}685>686Yes687</Button>688</Space>689{render_parallel()}690</div>691);692return (693<Alert694type="warning"695key={`${step}_confirm_to_all`}696style={{ marginTop: "15px" }}697message={message}698/>699);700}701702function copy_confirm_all_caution(step: AssignmentCopyStep) {703switch (step) {704case "assignment":705return (706<span>707This will recopy all of the files to them. CAUTION: if you update a708file that a student has also worked on, their work will get copied709to a backup file ending in a tilde, or possibly only be available in710snapshots. Select "Replace student files!" in case you do <b>not</b>{" "}711want to create any backups and also <b>delete</b> all other files in712the assignment folder of their projects.{" "}713<a714target="_blank"715href="https://github.com/sagemathinc/cocalc/wiki/CourseCopy"716>717(more details)718</a>719.720</span>721);722case "collect":723return "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.";724case "return_graded":725return "This will rereturn all of the graded files to them.";726case "peer_assignment":727return "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.";728case "peer_collect":729return "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.";730}731}732733function render_copy_confirm_overwrite_all(step: AssignmentCopyStep) {734return (735<div key={"copy_confirm_overwrite_all"} style={{ marginTop: "15px" }}>736<div style={{ marginBottom: "15px" }}>737{copy_confirm_all_caution(step)}738</div>739<Space wrap>740{render_copy_cancel(step)}741<Button742key={"all"}743type={"dashed"}744disabled={copy_assignment_confirm_overwrite}745onClick={() => copy_assignment(step, false)}746>747Yes, do it (with backup)748</Button>749{step === "assignment" ? (750<Button751key={"all-overwrite"}752type={"dashed"}753onClick={() => set_copy_assignment_confirm_overwrite(true)}754disabled={copy_assignment_confirm_overwrite}755>756Replace student files!757</Button>758) : undefined}759</Space>760{render_copy_assignment_confirm_overwrite(step)}761</div>762);763}764765function render_copy_confirm_to_all_or_new(step: AssignmentCopyStep, status) {766const n = status[`not_${step}`];767const m = n + status[step];768const message = (769<div>770<div style={{ marginBottom: "15px" }}>771{capitalize(step_verb(step))} this homework {step_direction(step)}772...773</div>774{render_has_student_subdir(step)}775{render_skip(step)}776<Space wrap>777{render_copy_cancel(step)}778<Button779key="all"780danger781onClick={() => {782set_copy_confirm_all(step, true);783set_copy_confirm(true);784}}785disabled={copy_confirm_all[step]}786>787{step === "assignment" ? "All" : "The"} {m} students788{step_ready(step, m)}...789</Button>790{n ? (791<Button792key="new"793type="primary"794onClick={() => copy_assignment(step, true)}795>796The {n} student{n > 1 ? "s" : ""} not already {step_verb(step)}797ed {step_direction(step)}798</Button>799) : undefined}800</Space>801{copy_confirm_all[step]802? render_copy_confirm_overwrite_all(step)803: undefined}804{render_parallel()}805</div>806);807return (808<Alert809type="warning"810key={`${step}_confirm_to_all_or_new`}811style={{ marginTop: "15px" }}812message={message}813/>814);815}816817function render_collect_tip() {818return (819<span key="normal">820<FormattedMessage821id="course.assignments.collect.tooltip"822defaultMessage={`Collect an assignment from all of your students.823(There is currently no way to schedule collection at a specific time;824instead, collection happens when you click the button.)`}825/>826</span>827);828}829830function render_button(state: AssignmentCopyStep, status) {831switch (state) {832case "collect":833return render_collect_button(status);834case "return_graded":835return render_return_graded_button(status);836case "peer_assignment":837return render_peer_assignment_button(status);838case "peer_collect":839return render_peer_collect_button(status);840case "assignment":841return render_assignment_button(status);842}843}844845function render_collect_button(status) {846if (status.assignment === 0) {847// no button if nothing ever assigned848return;849}850let type;851if (status.collect > 0) {852// Have already collected something853if (status.not_collect === 0) {854type = "dashed";855} else {856type = "default";857}858} else {859type = "primary";860}861return [862<Button863key="collect"864onClick={() => {865set_copy_confirm_state("collect", true);866set_copy_confirm(true);867}}868disabled={copy_confirm}869type={type}870size={size}871>872<Tip873title={874<span>875Collect: <Icon name="users" />{" "}876{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}877<Icon name="user-secret" /> You878</span>879}880tip={render_collect_tip()}881>882<Icon name="share-square" rotate={"180"} />{" "}883{intl.formatMessage(STEPS_INTL, {884step: STEP_NAMES.indexOf("Collect"),885})}886...887</Tip>888</Button>,889<Progress890key="progress"891done={status.collect}892not_done={status.not_collect}893step="collected"894skipped={assignment.get("skip_collect")}895/>,896];897}898899function render_peer_assign_tip() {900return (901<span key="normal">902Send copies of collected homework out to all students for peer grading.903</span>904);905}906907function render_peer_assignment_button(status) {908// Render the "Peer Assign..." button in the top row, for peer assigning to all909// students in the course.910if (status.peer_assignment == null) {911// not peer graded912return;913}914if (status.not_collect + status.not_assignment > 0) {915// collect everything before peer grading916return;917}918if (status.collect === 0) {919// nothing to peer assign920return;921}922let type;923if (status.peer_assignment > 0) {924// haven't peer-assigned anything yet925if (status.not_peer_assignment === 0) {926type = "dashed";927} else {928type = "default";929}930} else {931type = "primary";932}933const label = intl.formatMessage(STEPS_INTL, {934step: STEP_NAMES.indexOf("Peer Assign"),935});936return [937<Button938key="peer-assign"939onClick={() => {940set_copy_confirm_state("peer_assignment", true);941set_copy_confirm(true);942}}943disabled={copy_confirm}944type={type}945size={size}946>947<Tip948title={949<span>950{label}: <Icon name="users" /> {intl.formatMessage(labels.you)}{" "}951<Icon name="arrow-right" /> <Icon name="user-secret" />{" "}952{intl.formatMessage(course.students)}953</span>954}955tip={render_peer_assign_tip()}956>957<Icon name="share-square" /> {label}...958</Tip>959</Button>,960<Progress961key="progress"962done={status.peer_assignment}963not_done={status.not_peer_assignment}964step="peer assigned"965/>,966];967}968969function render_peer_collect_tip() {970return (971<span key="normal">Collect the peer grading that your students did.</span>972);973}974975function render_peer_collect_button(status) {976// Render the "Peer Collect..." button in the top row, for collecting peer grading from all977// students in the course.978if (status.peer_collect == null) {979return;980}981if (status.peer_assignment === 0) {982// haven't even peer assigned anything -- so nothing to collect983return;984}985if (status.not_peer_assignment > 0) {986// everybody must have received peer assignment, or collecting isn't allowed987return;988}989let type;990if (status.peer_collect > 0) {991// haven't peer-collected anything yet992if (status.not_peer_collect === 0) {993type = "dashed";994} else {995type = "default";996}997} else {998// warning, since we have already collected and this may overwrite999type = "primary";1000}1001const label = intl.formatMessage(STEPS_INTL, {1002step: STEP_NAMES.indexOf("Peer Collect"),1003});1004return [1005<Button1006key="peer-collect"1007onClick={() => {1008set_copy_confirm_state("peer_collect", true);1009set_copy_confirm(true);1010}}1011disabled={copy_confirm}1012type={type}1013size={size}1014>1015<Tip1016title={1017<span>1018{label}: <Icon name="users" />{" "}1019{intl.formatMessage(course.students)} <Icon name="arrow-right" />{" "}1020<Icon name="user-secret" /> You1021</span>1022}1023tip={render_peer_collect_tip()}1024>1025<Icon name="share-square" rotate="180" /> {label}...1026</Tip>1027</Button>,1028<Progress1029key="progress"1030done={status.peer_collect}1031not_done={status.not_peer_collect}1032step="peer collected"1033/>,1034];1035}10361037function toggle_skip_grading() {1038actions.assignments.set_skip(1039assignment.get("assignment_id"),1040"grading",1041!assignment.get("skip_grading"),1042);1043}10441045function render_skip_grading_button(status) {1046if (status.collect === 0) {1047// No button if nothing collected.1048return;1049}1050const icon: IconName = assignment.get("skip_grading")1051? "check-square-o"1052: "square-o";1053return (1054<Button onClick={toggle_skip_grading} size={size}>1055<Icon name={icon} /> Skip entering grades1056</Button>1057);1058}10591060function render_nbgrader_button(status) {1061if (1062status.collect === 0 ||1063!assignment.get("nbgrader") ||1064assignment.get("skip_grading")1065) {1066// No button if nothing collected or not nbgrader support or1067// decided to skip grading1068return;1069}10701071return (1072<NbgraderButton1073assignment_id={assignment.get("assignment_id")}1074name={name}1075/>1076);1077}10781079function render_return_graded_button(status) {1080if (status.collect === 0) {1081// No button if nothing collected.1082return;1083}1084if (status.peer_collect != null && status.peer_collect === 0) {1085// Peer grading enabled, but we didn't collect anything yet1086return;1087}1088if (1089!assignment.get("skip_grading") &&1090status.not_return_graded === 0 &&1091status.return_graded === 01092) {1093// Nothing unreturned and ungraded yet and also nothing returned yet1094return;1095}1096let type;1097if (status.return_graded > 0) {1098// Have already returned some1099if (status.not_return_graded === 0) {1100type = "dashed";1101} else {1102type = "default";1103}1104} else {1105type = "primary";1106}1107const label = intl.formatMessage(STEPS_INTL, {1108step: STEP_NAMES.indexOf("Return"),1109});1110return [1111<Button1112key="return"1113onClick={() => {1114set_copy_confirm_state("return_graded", true);1115set_copy_confirm(true);1116}}1117disabled={copy_confirm}1118type={type}1119size={size}1120>1121<Tip1122title={1123<span>1124{label}: <Icon name="user-secret" /> You{" "}1125<Icon name="arrow-right" /> <Icon name="users" />{" "}1126{intl.formatMessage(course.students)}{" "}1127</span>1128}1129tip="Copy the graded versions of files for this assignment from this project to all other student projects."1130>1131<Icon name="share-square" /> {label}...1132</Tip>1133</Button>,1134<Progress1135key="progress"1136done={status.return_graded}1137not_done={status.not_return_graded}1138step="returned"1139/>,1140];1141}11421143function delete_assignment() {1144actions.assignments.delete_assignment(assignment.get("assignment_id"));1145}11461147function undelete_assignment() {1148return actions.assignments.undelete_assignment(1149assignment.get("assignment_id"),1150);1151}11521153function render_delete_button() {1154if (assignment.get("deleted")) {1155return (1156<Tip1157key="delete"1158placement="left"1159title={intl.formatMessage({1160id: "course.assignment.undelete.title",1161defaultMessage: "Undelete assignment",1162})}1163tip={intl.formatMessage({1164id: "course.assignment.undelete.tooltip",1165defaultMessage:1166"Make the assignment visible again in the assignment list and in student grade lists.",1167})}1168>1169<Button onClick={undelete_assignment}>1170<Icon name="trash" /> {intl.formatMessage(labels.undelete)}1171</Button>1172</Tip>1173);1174} else {1175return (1176<Popconfirm1177title={1178<div style={{ maxWidth: "400px" }}>1179<FormattedMessage1180id="course.assignment.delete.confirm.info"1181defaultMessage={`<b>Are you sure you want to delete {name}"?</b>1182{br}1183This removes it from the assignment list and student grade lists,1184but does not delete any files off of disk.1185You can undelete an assignment later by showing it using the 'Show deleted assignments' button.`}1186values={{1187name: trunc_middle(assignment.get("path"), 24),1188br: <br />,1189}}1190/>1191</div>1192}1193onConfirm={delete_assignment}1194cancelText={intl.formatMessage(labels.cancel)}1195>1196<Button size={size}>1197<Icon name="trash" /> {intl.formatMessage(labels.delete)}...1198</Button>1199</Popconfirm>1200);1201}1202}12031204function render_configure_peer() {1205return <ConfigurePeerGrading actions={actions} assignment={assignment} />;1206}12071208function render_peer_button() {1209let icon;1210if (is_peer_graded()) {1211icon = "check-square-o";1212} else {1213icon = "square-o";1214}1215return (1216<Button1217disabled={expand_peer_config}1218onClick={() =>1219actions.toggle_item_expansion(1220"peer_config",1221assignment.get("assignment_id"),1222)1223}1224>1225<Icon name={icon} /> Peer Grading...1226</Button>1227);1228}12291230function render_summary_due_date() {1231const due_date = assignment.get("due_date");1232if (due_date) {1233return (1234<div style={{ marginTop: "12px" }}>1235Due <BigTime date={due_date} />1236</div>1237);1238}1239}12401241function render_assignment_name() {1242const num_items = assignment.get("listing")?.size ?? 0;1243return (1244<span>1245{trunc_middle(assignment.get("path"), 80)}1246{assignment.get("deleted") ? <b> (deleted)</b> : undefined}1247{num_items == 0 ? " - add content to this assignment..." : undefined}1248</span>1249);1250}12511252function render_assignment_title_link() {1253return (1254<a1255href=""1256onClick={(e) => {1257e.preventDefault();1258actions.toggle_item_expansion(1259"assignment",1260assignment.get("assignment_id"),1261);1262}}1263>1264<Icon1265style={{ marginRight: "10px" }}1266name={is_expanded ? "caret-down" : "caret-right"}1267/>1268{render_assignment_name()}1269</a>1270);1271}12721273function render_summary_line() {1274return (1275<Row key="summary" style={{ backgroundColor: background }}>1276<Col md={12}>1277<h5>{render_assignment_title_link()}</h5>1278</Col>1279<Col md={12}>{render_summary_due_date()}</Col>1280</Row>1281);1282}12831284return (1285<div>1286<Row style={is_expanded ? styles.selected_entry : styles.entry_style}>1287<Col xs={24}>1288{render_summary_line()}1289{is_expanded ? render_more() : undefined}1290</Col>1291</Row>1292</div>1293);1294}129512961297