Skip to content

Commit 4653249

Browse files
authored
Gradescope update (#2053)
2 parents cf38c33 + 42194f7 commit 4653249

File tree

17 files changed

+258
-157
lines changed

17 files changed

+258
-157
lines changed

INSTRUCTORS.md

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,8 @@ between the two without clearing your local storage (in browser dev tools).
3737

3838
NOTE: This is only relevant to the EECS490 repo (which include the haz3lschool build target).
3939

40-
1. Open the exercise in instructor mode and export a grading version (button in top bar) which generates an OCaml file.
40+
1. Update the `src/haz3lschool/Specs.re` module with `<module_name>.exercise`.
4141

42-
2. Move the file to `src/haz3lschool/specs`.
42+
2. Run `make grade SUBMISSION=<path to submission json>` under project root to generate a grade report.
4343

44-
3. Update the `src/haz3lschool/Specs.re` module with `<module_name>.exercise`.
45-
46-
4. Run `dune exec ./src/haz3lschool/gradescope.exe <path_to_student_json>` under project root to print the grade report.
47-
48-
To change the output format, adjust `Main.gen_grading_report` function in `Gradescope.re` .
44+
TODO: currently batch grading, export to gradescope, and autograder generation are have regressed. Next time 490 is taught, these can be added by consulting with commit b715dba2ce2949b7277a2d4de0451ccbea6a878c.

Makefile

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ TEST_DIR="$(shell pwd)/_build/default/test"
22
HTML_DIR="$(shell pwd)/_build/default/src/web/www"
33
SERVER="http://0.0.0.0:8000/"
44

5-
.PHONY: all deps change-deps setup-instructor setup-student dev dev-helper dev-student fmt watch watch-release release release-student echo-html-dir serve serve2 repl test clean
5+
.PHONY: all deps change-deps setup-instructor setup-student dev dev-helper dev-student fmt watch watch-release release release-student grade echo-html-dir serve serve2 repl test clean
66

77
all: dev
88

@@ -48,6 +48,12 @@ release: setup-instructor
4848
release-student: setup-student
4949
dune build @src/fmt --auto-promote src --profile dev # Uses dev profile for performance reasons. It may be worth it to retest since the ocaml upgrade
5050

51+
grade:
52+
ifndef SUBMISSION
53+
$(error Usage: make grade SUBMISSION=<path to submission json>)
54+
endif
55+
python3 src/grading/grade/grade_individual.py $(SUBMISSION) .
56+
5157
echo-html-dir:
5258
@echo $(HTML_DIR)
5359

OUTPUT.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[
2+
{
3+
"name": "Oddly Recursive",
4+
"report": {
5+
"summary": "Overall: 0.0/4.0\n\nTest Validation: 0.0/1.0\n\nSource Code:\n\ntest not(false) end;\ntest not(not(true)) end; \n \n\nMutation Testing: 0.0/1.0\n\nImpl Grading: 0.0/2.0\n\nSource Code:\n\nlet odd: Int -> Bool =\n fun n -> if n < 0 then odd(-n) else \n if n == 0 then true else not(odd(n-1)) \nin \n\n",
6+
"overall": [ 0.0, 4.0 ]
7+
}
8+
},
9+
{
10+
"name": "Recursive Fibonacci",
11+
"report": {
12+
"summary": "Overall: 0.0/4.0\n\nTest Validation: 0.0/1.0\n\nSource Code:\n\n \n\nMutation Testing: 0.0/1.0\n\nImpl Grading: 0.0/2.0\n\nSource Code:\n\nlet fib : Int -> Int = \n fun n -> \nin \n\n",
13+
"overall": [ 0.0, 4.0 ]
14+
}
15+
}
16+
]

package-lock.json

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"@esbuild-plugins/node-resolve": "^0.2.2",
1414
"hotkeys-js": "^3.8.7",
1515
"ninja-keys": "^1.2.2",
16-
"algebrite": "^1.4.0"
16+
"algebrite": "^1.4.0",
17+
"web-worker": "^1.5.0"
1718
},
1819
"devDependencies": {
1920
"@types/node": "^22.14.0",
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
#!/usr/bin/env python3
2+
3+
import json
4+
import sys
5+
import subprocess
6+
import tempfile
7+
from pathlib import Path
8+
import argparse
9+
10+
def run_hazel_grader(student_json_str, hazel_path):
11+
"""
12+
Run the Hazel grader on student JSON
13+
Uses: node -r ./src/web/www/polyfill_worker.js _build/default/src/web/gradingReport.bc.js <input> <output>
14+
"""
15+
16+
# Find the required Hazel files
17+
polyfill_file = hazel_path / "src/web/www/polyfill_worker.js"
18+
grading_file = hazel_path / "_build/default/src/web/gradingReport.bc.js"
19+
20+
if not polyfill_file.exists():
21+
raise FileNotFoundError(f"Hazel polyfill not found: {polyfill_file}")
22+
if not grading_file.exists():
23+
raise FileNotFoundError(f"Hazel gradingReport.bc.js not found: {grading_file}")
24+
25+
with tempfile.TemporaryDirectory() as tmpdir:
26+
input_file = Path(tmpdir) / "input.json"
27+
output_file = Path(tmpdir) / "output.json"
28+
29+
# Write input JSON
30+
with open(input_file, 'w') as f:
31+
decoded = json.loads(student_json_str)
32+
json.dump(decoded, f, indent=2)
33+
34+
# Run Hazel grader
35+
cmd = [
36+
"node",
37+
"-r", str(polyfill_file),
38+
str(grading_file),
39+
str(input_file),
40+
str(output_file)
41+
]
42+
43+
try:
44+
result = subprocess.run(
45+
cmd,
46+
cwd=hazel_path / "_build/default/src/web/www/",
47+
capture_output=True,
48+
text=True,
49+
check=True
50+
)
51+
52+
# Read output
53+
if output_file.exists():
54+
with open(output_file, 'r') as f:
55+
return f.read().strip()
56+
else:
57+
return result.stdout.strip()
58+
59+
except subprocess.CalledProcessError as e:
60+
print(f"[error] Hazel grader failed: {e.stderr}", file=sys.stderr)
61+
raise
62+
63+
64+
def hazel_transform(value, hazel_path):
65+
"""Transform function for Hazel grading"""
66+
try:
67+
return run_hazel_grader(value, hazel_path)
68+
except Exception as e:
69+
print(f"[error] Hazel processing failed: {e}", file=sys.stderr)
70+
return "Hazel grader error"
71+
72+
73+
def main():
74+
parser = argparse.ArgumentParser(
75+
description="Process a single exercise submission through the Hazel grader",
76+
formatter_class=argparse.RawDescriptionHelpFormatter)
77+
78+
parser.add_argument("submission_file", help="Path to submission JSON")
79+
parser.add_argument("hazel_dir", help="Path to Hazel project directory")
80+
parser.add_argument("--output", "-o", help="Output file (default: stdout)")
81+
82+
args = parser.parse_args()
83+
84+
submission_path = Path(args.submission_file).resolve()
85+
hazel_path = Path(args.hazel_dir).resolve()
86+
87+
if not submission_path.exists():
88+
print(f"Error: Submission file {submission_path} not found", file=sys.stderr)
89+
sys.exit(1)
90+
91+
if not hazel_path.exists():
92+
print(f"Error: Hazel directory {hazel_path} not found", file=sys.stderr)
93+
sys.exit(1)
94+
95+
try:
96+
with open(submission_path, 'r') as f:
97+
submission = f.read()
98+
result = hazel_transform(submission, hazel_path)
99+
100+
# Output result
101+
json_output = json.dumps(result, indent=2, sort_keys=True)
102+
103+
if args.output:
104+
with open(args.output, 'w') as f:
105+
f.write(json_output)
106+
print(f"Results written to {args.output}", file=sys.stderr)
107+
else:
108+
print(json_output)
109+
110+
except Exception as e:
111+
print(f"Error: {e}", file=sys.stderr)
112+
sys.exit(1)
113+
114+
115+
if __name__ == "__main__":
116+
main()

src/web/app/editors/mode/ExercisesMode.re

Lines changed: 2 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,6 @@ module Model = {
148148
| Theorem(_) => "(* Theorem exercises do not have an exportable transitionary module *)\n"
149149
};
150150

151-
let export_grading_module = (e: exercise): string =>
152-
switch (e) {
153-
| Implementation(e) =>
154-
Exercise.export_grading_module(e.editors.module_name, {eds: e.editors})
155-
| Theorem(_) => "(* Theorem exercises do not have an exportable grading module *)\n"
156-
};
157-
158151
// Used for the assistant or something
159152
let get_editor = (model: t): CodeEditable.Model.t => {
160153
let current = List.nth(model.exercises, model.current);
@@ -290,8 +283,7 @@ module Update = {
290283
| TheoremExercise(TheoremExerciseMode.Update.t)
291284
| ExportModule
292285
| ExportSubmission
293-
| ExportTransitionary
294-
| ExportGrading;
286+
| ExportTransitionary;
295287

296288
let can_undo = (action: t) => {
297289
switch (action) {
@@ -301,7 +293,6 @@ module Update = {
301293
| ExportModule => false
302294
| ExportSubmission => false
303295
| ExportTransitionary => false
304-
| ExportGrading => false
305296
};
306297
};
307298
let export_exercise_module = (exercises: Model.t): unit => {
@@ -335,15 +326,6 @@ module Update = {
335326
JsUtil.download_string_file(~filename, ~content_type, ~contents);
336327
};
337328

338-
let export_instructor_grading_report = (exercises: Model.t) => {
339-
let exercise = Model.get_current(exercises);
340-
// .ml files because show uses OCaml syntax (dune handles seamlessly)
341-
let filename = (exercise |> Model.get_exercise_name) ++ "_grading.ml";
342-
let content_type = "text/plain";
343-
let contents = Model.export_grading_module(exercise);
344-
JsUtil.download_string_file(~filename, ~content_type, ~contents);
345-
};
346-
347329
let update =
348330
(~globals: Globals.t, ~schedule_action, action: t, model: Model.t) => {
349331
switch (Model.get_current(model), action) {
@@ -402,10 +384,6 @@ module Update = {
402384
Store.save(~instructor_mode=globals.settings.instructor_mode, model);
403385
export_transitionary(model);
404386
model |> return_quiet;
405-
| (_, ExportGrading) =>
406-
Store.save(~instructor_mode=globals.settings.instructor_mode, model);
407-
export_instructor_grading_report(model);
408-
model |> return_quiet;
409387
};
410388
};
411389

@@ -571,13 +549,6 @@ module View = {
571549
~tooltip="Export Transitionary Exercise Module",
572550
);
573551

574-
let instructor_grading_export =
575-
Widgets.button_named(
576-
Icons.export,
577-
_ => {inject(ExportGrading)},
578-
~tooltip="Export Grading Exercise Module",
579-
);
580-
581552
let export_submission =
582553
Widgets.button_named(
583554
Icons.star,
@@ -640,11 +611,7 @@ module View = {
640611
NutMenu.item_group(
641612
~inject,
642613
"Developer Export",
643-
[
644-
instructor_export,
645-
instructor_transitionary_export,
646-
instructor_grading_export,
647-
],
614+
[instructor_export, instructor_transitionary_export],
648615
);
649616

650617
if (globals.settings.instructor_mode) {

src/web/app/editors/mode/TutorialsMode.re

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ module Update = {
170170
| Tutorial(TutorialMode.Update.t)
171171
| ExportModule
172172
| ExportSubmission
173-
| ExportTransitionary
174-
| ExportGrading;
173+
| ExportTransitionary;
175174

176175
let can_undo = (action: t) => {
177176
switch (action) {
@@ -180,7 +179,6 @@ module Update = {
180179
| ExportModule => false
181180
| ExportSubmission => false
182181
| ExportTransitionary => false
183-
| ExportGrading => false
184182
};
185183
};
186184

@@ -216,16 +214,6 @@ module Update = {
216214
);
217215
JsUtil.download_string_file(~filename, ~content_type, ~contents);
218216
};
219-
let export_instructor_grading_report = (exercises: Model.t) => {
220-
let exercise = Model.get_current(exercises);
221-
// .ml files because show uses OCaml syntax (dune handles seamlessly)
222-
let module_name = exercise.editors.module_name;
223-
let filename = exercise.editors.module_name ++ "_grading.ml";
224-
let content_type = "text/plain";
225-
let contents =
226-
Tutorial.export_grading_module(module_name, {eds: exercise.editors});
227-
JsUtil.download_string_file(~filename, ~content_type, ~contents);
228-
};
229217

230218
let update =
231219
(~globals: Globals.t, ~schedule_action, action: t, model: Model.t) => {
@@ -280,10 +268,6 @@ module Update = {
280268
Store.save(~instructor_mode=globals.settings.instructor_mode, model);
281269
export_transitionary(model);
282270
model |> return_quiet;
283-
| ExportGrading =>
284-
Store.save(~instructor_mode=globals.settings.instructor_mode, model);
285-
export_instructor_grading_report(model);
286-
model |> return_quiet;
287271
};
288272
};
289273
let calculate =
@@ -371,12 +355,6 @@ module View = {
371355
_ => {inject(ExportTransitionary)},
372356
~tooltip="Export Transitionary Exercise Module",
373357
);
374-
let instructor_grading_export =
375-
Widgets.button_named(
376-
Icons.export,
377-
_ => {inject(ExportGrading)},
378-
~tooltip="Export Grading Exercise Module",
379-
);
380358
let export_submission =
381359
Widgets.button_named(
382360
Icons.star,
@@ -433,11 +411,7 @@ module View = {
433411
NutMenu.item_group(
434412
~inject,
435413
"Developer Export",
436-
[
437-
instructor_export,
438-
instructor_transitionary_export,
439-
instructor_grading_export,
440-
],
414+
[instructor_export, instructor_transitionary_export],
441415
);
442416
if (globals.settings.instructor_mode) {
443417
[

src/web/app/input/Shortcut.re

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,6 @@ let instructor_shortcuts: list(t) = [
3131
"Export Transitionary Exercise Module",
3232
Editors(Exercises(ExportTransitionary)) // TODO Would we rather skip contextual stuff for now or include it and have it fail
3333
),
34-
mk_shortcut(
35-
~mdIcon="download",
36-
~section="Export",
37-
"Export Grading Exercise Module",
38-
Editors(Exercises(ExportGrading)) // TODO Would we rather skip contextual stuff for now or include it and have it fail
39-
),
4034
];
4135

4236
// List of shortcuts configured to show up in the command palette and have hotkey support

0 commit comments

Comments
 (0)