Path: blob/master/src/packages/frontend/course/nbgrader/scores.tsx
5997 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Component that shows all the scores for all problems and notebooks in a given assignment.7*/89import { Alert, Button, Card } from "antd";10import { Icon } from "@cocalc/frontend/components";11import { useActions } from "@cocalc/frontend/app-framework";12import { ReactNode, useState } from "react";13import {14NotebookScores,15Score,16} from "@cocalc/frontend/jupyter/nbgrader/autograde";17import { get_nbgrader_score } from "../store";18import { CourseActions } from "../actions";19import { autograded_filename } from "../util";2021interface Props {22nbgrader_scores: { [ipynb: string]: NotebookScores | string };23nbgrader_score_ids?: { [ipynb: string]: string[] };24assignment_id: string;25student_id: string;26name: string;27show_all?: boolean;28set_show_all?: () => void;29run_button?: ReactNode;30buttonSize?: "small" | "middle" | "large";31}3233interface State {34filename?: string;35id?: string;36}3738export function NbgraderScores({39nbgrader_scores,40nbgrader_score_ids,41assignment_id,42student_id,43name,44show_all,45set_show_all,46run_button,47buttonSize,48}: Props) {49const actions = useActions<CourseActions>({ name });5051const [editingScore, setEditingScore] = useState<State>({});5253function render_show_all() {54if (!show_all) return;55const v: React.JSX.Element[] = [];56for (const filename in nbgrader_scores) {57v.push(render_info_for_file(filename, nbgrader_scores[filename]));58}59return <div>{v}</div>;60}6162function render_info_for_file(63filename: string,64scores: NotebookScores | string,65) {66return (67<div key={filename} style={{ marginBottom: "5px" }}>68{render_filename_links(filename)}69{render_scores_for_file(filename, scores)}70</div>71);72}7374function open_filename(filename: string): void {75actions.assignments.open_file_in_collected_assignment(76assignment_id,77student_id,78filename,79);80}8182function render_filename_links(filename: string) {83const filename2 = autograded_filename(filename);84return (85<div style={{ fontSize: "12px" }}>86<a87style={{ fontFamily: "monospace" }}88onClick={() => open_filename(filename)}89>90{filename}91</a>92<br />93<a94style={{ fontFamily: "monospace" }}95onClick={() => open_filename(filename2)}96>97{filename2}98</a>99</div>100);101}102103function render_scores_for_file(104filename: string,105scores: NotebookScores | string,106) {107if (typeof scores == "string") {108return (109<Alert110type="error"111message={scores + "\n- try running nbgrader again."}112/>113);114}115const v: React.JSX.Element[] = [];116117const ids: string[] = nbgrader_score_ids?.[filename] ?? [];118for (const id in scores) {119if (!ids.includes(id)) {120ids.push(id);121}122}123124for (const id of ids) {125if (scores[id] != null) {126v.push(render_score(filename, id, scores[id]));127}128}129130const style = { padding: "5px" };131return (132<table133style={{134border: "1px solid lightgray",135width: "100%",136borderRadius: "3px",137borderCollapse: "collapse",138}}139>140<thead>141<tr key={"header"} style={{ border: "1px solid grey" }}>142<th style={style}>Problem</th>143<th style={style}>Score</th>144</tr>145</thead>146<tbody>{v}</tbody>147</table>148);149}150151function set_score(filename: string, id: string, value: string): void {152const score = parseFloat(value);153if (isNaN(score) || !isFinite(score)) {154return; // invalid scores gets thrown away155}156actions.assignments.set_specific_nbgrader_score(157assignment_id,158student_id,159filename,160id,161score,162true,163);164}165166function render_assigned_score(filename: string, id: string, score: Score) {167if (!score.manual) {168return <>{score.score ?? "?"}</>;169}170171const value = `${score.score != null ? score.score : ""}`;172const style = {173width: "48px",174color: "#666",175fontSize: "14px",176border: "1px solid lightgrey",177display: "inline-block",178padding: "1px",179};180if (editingScore.filename == filename && editingScore.id == id) {181return (182<input183spellCheck={false}184autoFocus185type="input"186defaultValue={value}187onBlur={(e) => stop_editing_score((e.target as any).value)}188style={style}189/>190);191} else {192return (193<span style={style} onClick={() => setEditingScore({ filename, id })}>194{value ? value : "-"}195</span>196);197}198}199200function stop_editing_score(value: string): void {201if (editingScore.id != null && editingScore.filename != null) {202set_score(editingScore.filename, editingScore.id, value);203}204setEditingScore({205filename: undefined,206id: undefined,207});208}209210function render_score(filename: string, id: string, score: Score) {211const backgroundColor = score.score == null ? "#fff1f0" : undefined;212const style = { padding: "5px", backgroundColor };213return (214<tr key={id}>215<td style={style}>{id}</td>216<td style={style}>217{render_assigned_score(filename, id, score)} / {score.points}218{render_needs_score(score)}219</td>220</tr>221);222}223224function render_needs_score(score: Score) {225if (!score.manual || score.score != null) return;226return (227<div>228<Icon name="exclamation-triangle" /> Enter score above229</div>230);231}232233function render_more_toggle({234hasScores,235manual_needed,236error,237buttonSize,238}: {239hasScores: boolean;240manual_needed: boolean;241error: boolean;242buttonSize?: "small" | "middle" | "large";243}) {244if (!hasScores) return undefined;245const showManual = manual_needed || error;246const editButton = (247<Button248size={buttonSize}249icon={<Icon name="pencil" />}250aria-label="Edit nbgrader scores"251onClick={() => set_show_all?.()}252/>253);254return (255<span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>256{showManual ? <span>Manual</span> : null}257{editButton}258</span>259);260}261262function render_title(content: string) {263return (264<span>265<b>nbgrader:</b> {content}266</span>267);268}269270const { score, points, error = false, manual_needed } =271get_nbgrader_score(nbgrader_scores);272273const hasScores = Object.keys(nbgrader_scores ?? {}).length > 0;274const action_required: boolean = !!(275!show_all &&276(manual_needed || error || !hasScores)277);278279const backgroundColor = action_required ? "#fff1f0" : undefined;280281const titleContent = hasScores ? (error ? "error" : `${score}/${points}`) : "--/--";282283const titleWithRun =284run_button == null ? (285render_title(titleContent)286) : (287<div style={{ display: "flex", alignItems: "center", gap: "8px" }}>288{run_button}289{render_title(titleContent)}290</div>291);292293return (294<Card295size="small"296style={{ marginTop: "5px", backgroundColor }}297extra={render_more_toggle({298hasScores,299manual_needed,300error,301buttonSize,302})}303title={titleWithRun}304styles={{ body: show_all ? {} : { padding: 0 } }}305>306{render_show_all()}307</Card>308);309}310311312