Skip to content

Commit 0a2228b

Browse files
authored
Empty submission detection (#570)
1 parent 5be9cb4 commit 0a2228b

File tree

13 files changed

+709
-28
lines changed

13 files changed

+709
-28
lines changed

app/course/[course_id]/manage/assignments/new/form.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,26 @@ export default function AssignmentForm({
742742
</Checkbox.Root>
743743
</Field>
744744
</Fieldset.Content>
745+
<Fieldset.Content>
746+
<Field helperText="When enabled, students can submit even if their files match the handout (starter) exactly. When disabled, such empty submissions are rejected with a message asking them to commit their changes.">
747+
<Controller
748+
name="permit_empty_submissions"
749+
control={control}
750+
render={({ field }) => (
751+
<Checkbox.Root
752+
checked={field.value !== false}
753+
onCheckedChange={(checked) => field.onChange(!!checked.checked)}
754+
>
755+
<Checkbox.HiddenInput />
756+
<Checkbox.Control>
757+
<LuCheck />
758+
</Checkbox.Control>
759+
<Checkbox.Label>Permit empty submissions (match handout exactly)</Checkbox.Label>
760+
</Checkbox.Root>
761+
)}
762+
/>
763+
</Field>
764+
</Fieldset.Content>
745765
<Fieldset.Content>
746766
<Field
747767
label="Handout URL"

app/course/[course_id]/manage/assignments/new/page.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ export default function NewAssignmentPage() {
2121
const form = useForm<Assignment>({
2222
refineCoreProps: { resource: "assignments", action: "create" },
2323
defaultValues: {
24-
allow_not_graded_submissions: true
24+
allow_not_graded_submissions: true,
25+
permit_empty_submissions: true
2526
}
2627
});
2728
const router = useRouter();
@@ -88,6 +89,7 @@ export default function NewAssignmentPage() {
8889
description: getValues("description"),
8990
max_late_tokens: getValues("max_late_tokens") || null,
9091
allow_not_graded_submissions: getValues("allow_not_graded_submissions"),
92+
permit_empty_submissions: getValues("permit_empty_submissions") !== false,
9193
total_points: getValues("total_points"),
9294
template_repo: getValues("template_repo"),
9395
submission_files: getValues("submission_files"),

package-lock.json

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

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"test:watch": "jest --watch",
1717
"test:coverage": "jest --coverage",
1818
"test:e2e": "npx playwright test",
19+
"test:e2e:local": "cross-env BASE_URL=http://localhost:3000 npx playwright test",
1920
"test:k6:build": "webpack --config webpack.k6.config.js",
2021
"test:k6:submissions-api": "npm run test:k6:build && k6 run dist/k6-tests/submissions-api.js -e SUPABASE_URL=$SUPABASE_URL -e SUPABASE_SERVICE_ROLE_KEY=$SUPABASE_SERVICE_ROLE_KEY -e END_TO_END_SECRET=$END_TO_END_SECRET ",
2122
"client": "npx supabase gen types typescript --project-id 'pveyalbiqnrpvuazgyuo' --schema public,pgmq_public > utils/supabase/SupabaseTypes.d.ts && cp utils/supabase/SupabaseTypes.d.ts supabase/functions/_shared/SupabaseTypes.d.ts",

supabase/functions/_shared/GitHubWrapper.ts

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,13 @@ async function retryWithBackoff<T>(
7272
} catch (error: unknown) {
7373
lastError = error as Error;
7474

75-
// Check if this is a 404 error that we should retry
75+
// Check if this is an error we should retry (404 or "Git Repository is empty")
7676
const is404 = error instanceof RequestError && error.status === 404;
77+
const isGitRepoEmpty = error instanceof Error && error.message?.toLowerCase().includes("git repository is empty");
78+
const shouldRetry = is404 || isGitRepoEmpty;
7779

78-
if (!is404 || attempt === maxRetries) {
79-
// Don't retry for non-404 errors or if we've exhausted retries
80+
if (!shouldRetry || attempt === maxRetries) {
81+
// Don't retry for non-retryable errors or if we've exhausted retries
8082
if (attempt > 0) {
8183
scope?.setContext("retry_failed", {
8284
final_attempt: attempt + 1,
@@ -89,7 +91,7 @@ async function retryWithBackoff<T>(
8991
tags: {
9092
operation: "github_api_retry_failed",
9193
attempts: attempt + 1,
92-
error_type: is404 ? "404_not_found" : "other"
94+
error_type: is404 ? "404_not_found" : isGitRepoEmpty ? "git_repo_empty" : "other"
9395
}
9496
});
9597
}
@@ -102,23 +104,25 @@ async function retryWithBackoff<T>(
102104
scope?.setContext("retry_attempt", {
103105
attempt: attempt + 1,
104106
next_delay_ms: delayMs,
105-
error_status: 404,
107+
error_status: error instanceof RequestError ? error.status : "unknown",
108+
error_reason: is404 ? "404" : "git_repo_empty",
106109
operation: "github_api_retry"
107110
});
108111

109112
Sentry.addBreadcrumb({
110-
message: `GitHub API 404 error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`,
113+
message: `GitHub API ${is404 ? "404" : "Git Repository is empty"} error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1})`,
111114
level: "warning",
112115
data: {
113116
attempt: attempt + 1,
114117
delay_ms: delayMs,
115-
error_status: 404,
118+
error_status: error instanceof RequestError ? error.status : "unknown",
119+
error_reason: is404 ? "404" : "git_repo_empty",
116120
error_message: error instanceof Error ? error.message : String(error)
117121
}
118122
});
119123

120124
console.log(
121-
`GitHub API 404 error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1}):`,
125+
`GitHub API ${is404 ? "404" : "Git Repository is empty"} error, retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries + 1}):`,
122126
error instanceof Error ? error.message : String(error)
123127
);
124128

@@ -753,8 +757,8 @@ export async function createRepo(
753757
owner: org,
754758
repo: repoName
755759
}),
756-
3, // maxRetries
757-
1000, // baseDelayMs
760+
5, // maxRetries
761+
3000, // baseDelayMs
758762
scope
759763
);
760764
scope?.setTag("head_sha", heads.data.object.sha);

supabase/functions/_shared/SupabaseTypes.d.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,79 @@ export type Database = {
770770
}
771771
];
772772
};
773+
assignment_handout_file_hashes: {
774+
Row: {
775+
assignment_id: number;
776+
class_id: number;
777+
combined_hash: string;
778+
created_at: string;
779+
file_hashes: Json;
780+
id: number;
781+
sha: string;
782+
};
783+
Insert: {
784+
assignment_id: number;
785+
class_id: number;
786+
combined_hash: string;
787+
created_at?: string;
788+
file_hashes?: Json;
789+
id?: number;
790+
sha: string;
791+
};
792+
Update: {
793+
assignment_id?: number;
794+
class_id?: number;
795+
combined_hash?: string;
796+
created_at?: string;
797+
file_hashes?: Json;
798+
id?: number;
799+
sha?: string;
800+
};
801+
Relationships: [
802+
{
803+
foreignKeyName: "assignment_handout_file_hashes_assignment_id_fkey";
804+
columns: ["assignment_id"];
805+
isOneToOne: false;
806+
referencedRelation: "assignment_overview";
807+
referencedColumns: ["id"];
808+
},
809+
{
810+
foreignKeyName: "assignment_handout_file_hashes_assignment_id_fkey";
811+
columns: ["assignment_id"];
812+
isOneToOne: false;
813+
referencedRelation: "assignments";
814+
referencedColumns: ["id"];
815+
},
816+
{
817+
foreignKeyName: "assignment_handout_file_hashes_assignment_id_fkey";
818+
columns: ["assignment_id"];
819+
isOneToOne: false;
820+
referencedRelation: "assignments_for_student_dashboard";
821+
referencedColumns: ["id"];
822+
},
823+
{
824+
foreignKeyName: "assignment_handout_file_hashes_assignment_id_fkey";
825+
columns: ["assignment_id"];
826+
isOneToOne: false;
827+
referencedRelation: "assignments_with_effective_due_dates";
828+
referencedColumns: ["id"];
829+
},
830+
{
831+
foreignKeyName: "assignment_handout_file_hashes_assignment_id_fkey";
832+
columns: ["assignment_id"];
833+
isOneToOne: false;
834+
referencedRelation: "submissions_with_grades_for_assignment_and_regression_test";
835+
referencedColumns: ["assignment_id"];
836+
},
837+
{
838+
foreignKeyName: "assignment_handout_file_hashes_class_id_fkey";
839+
columns: ["class_id"];
840+
isOneToOne: false;
841+
referencedRelation: "classes";
842+
referencedColumns: ["id"];
843+
}
844+
];
845+
};
773846
assignment_leaderboard: {
774847
Row: {
775848
assignment_id: number;
@@ -948,6 +1021,7 @@ export type Database = {
9481021
meta_grading_rubric_id: number | null;
9491022
min_group_size: number | null;
9501023
minutes_due_after_lab: number | null;
1024+
permit_empty_submissions: boolean;
9511025
regrade_deadline: string | null;
9521026
release_date: string | null;
9531027
self_review_rubric_id: number | null;
@@ -984,6 +1058,7 @@ export type Database = {
9841058
meta_grading_rubric_id?: number | null;
9851059
min_group_size?: number | null;
9861060
minutes_due_after_lab?: number | null;
1061+
permit_empty_submissions?: boolean;
9871062
regrade_deadline?: string | null;
9881063
release_date?: string | null;
9891064
self_review_rubric_id?: number | null;
@@ -1020,6 +1095,7 @@ export type Database = {
10201095
meta_grading_rubric_id?: number | null;
10211096
min_group_size?: number | null;
10221097
minutes_due_after_lab?: number | null;
1098+
permit_empty_submissions?: boolean;
10231099
regrade_deadline?: string | null;
10241100
release_date?: string | null;
10251101
self_review_rubric_id?: number | null;
@@ -8562,6 +8638,7 @@ export type Database = {
85628638
grading_review_id: number | null;
85638639
id: number;
85648640
is_active: boolean;
8641+
is_empty_submission: boolean;
85658642
is_not_graded: boolean;
85668643
ordinal: number;
85678644
profile_id: string | null;
@@ -8581,6 +8658,7 @@ export type Database = {
85818658
grading_review_id?: number | null;
85828659
id?: number;
85838660
is_active?: boolean;
8661+
is_empty_submission?: boolean;
85848662
is_not_graded?: boolean;
85858663
ordinal?: number;
85868664
profile_id?: string | null;
@@ -8600,6 +8678,7 @@ export type Database = {
86008678
grading_review_id?: number | null;
86018679
id?: number;
86028680
is_active?: boolean;
8681+
is_empty_submission?: boolean;
86038682
is_not_graded?: boolean;
86048683
ordinal?: number;
86058684
profile_id?: string | null;

0 commit comments

Comments
 (0)