+ {{ $t("project.unmet_mandatory") }}
-
- {{ $t("project.unmet_forbidden") }}
-
-
+ {{ $t("project.unmet_forbidden") }}
-
-import { computed, toRefs } from "vue";
+import { computed, ref, toRefs } from "vue";
import type { Requirement, UnmetRequirement } from "@/models/Project";
const props = defineProps<{
@@ -80,8 +78,8 @@ const props = defineProps<{
const { requirements, unmetRequirements } = toRefs(props);
-const mandatory = computed(() => requirements.value.filter((r) => r.mandatory));
-const forbidden = computed(() => requirements.value.filter((r) => !r.mandatory));
+const mandatory = ref(requirements.value.filter((r) => r.mandatory));
+const forbidden = ref(requirements.value.filter((r) => !r.mandatory));
const unmet_extensions = computed(() => unmetRequirements.value.map((r) => r.requirement.value));
diff --git a/frontend/src/components/submission/SubmitForm.vue b/frontend/src/components/submission/SubmitForm.vue
index 6f45e9ce..de707757 100644
--- a/frontend/src/components/submission/SubmitForm.vue
+++ b/frontend/src/components/submission/SubmitForm.vue
@@ -17,7 +17,7 @@
import { computed, ref, toRefs } from "vue";
import FilesInput from "@/components/form_elements/FilesInput.vue";
import { useRouter } from "vue-router";
-import { useCreateSubmissionMutation } from "@/queries/Submission";
+import { UnmetRequirementsError, useCreateSubmissionMutation } from "@/queries/Submission";
import { useProjectGroupQuery } from "@/queries/Group";
import { useI18n } from "vue-i18n";
import RequirementsCard from "@/components/project/RequirementsCard.vue";
@@ -57,10 +57,10 @@ async function formOnSubmit(event: SubmitEvent) {
await mutateAsync(formData);
await router.push(`/groups/${group.value?.id}/submissions`);
} catch (error) {
- if (error instanceof Error) {
- unmetRequirements.value = error.cause.map((r) => {
- return { requirement: { mandatory: r.type === "mandatory", value: r.requirement }, files: r.files };
- });
+ if (error instanceof UnmetRequirementsError) {
+ unmetRequirements.value = error.unmetRequirements;
+ } else {
+ throw error;
}
}
}
diff --git a/frontend/src/queries/Group.ts b/frontend/src/queries/Group.ts
index bfe7fcd8..9975dafd 100644
--- a/frontend/src/queries/Group.ts
+++ b/frontend/src/queries/Group.ts
@@ -78,7 +78,7 @@ export function useProjectGroupQuery(
return useQuery({
queryKey: computed(() => PROJECT_USER_GROUP_QUERY_KEY(toValue(projectId)!)),
queryFn: () => getGroupWithProjectId(groups.value!, toValue(projectId)!),
- enabled: !!toValue(projectId) && groups.value !== undefined,
+ enabled: !!toValue(projectId),
});
}
diff --git a/frontend/src/queries/Submission.ts b/frontend/src/queries/Submission.ts
index ec006edd..b13c6649 100644
--- a/frontend/src/queries/Submission.ts
+++ b/frontend/src/queries/Submission.ts
@@ -5,6 +5,8 @@ import type { UseMutationReturnType, UseQueryReturnType } from "@tanstack/vue-qu
import { createSubmission, getFiles, getSubmission, getSubmissions } from "@/services/submission";
import type Submission from "@/models/Submission";
import type FileInfo from "@/models/File";
+import { FetchError } from "@/services";
+import type { UnmetRequirement } from "@/models/Project";
function SUBMISSION_QUERY_KEY(submissionId: number): (string | number)[] {
return ["submission", submissionId];
@@ -59,6 +61,15 @@ export function useFilesQuery(
});
}
+export class UnmetRequirementsError extends Error {
+ unmetRequirements: UnmetRequirement[];
+
+ constructor(message: string, unmetRequirements: UnmetRequirement[], ...params: any[]) {
+ super(message, ...params);
+ this.unmetRequirements = unmetRequirements;
+ }
+}
+
/**
* Mutation composable for creating a submission
*/
@@ -67,13 +78,44 @@ export function useCreateSubmissionMutation(
): UseMutationReturnType, void> {
const queryClient = useQueryClient();
return useMutation({
- mutationFn: (formData) => createSubmission(toValue(groupId)!, toValue(formData)),
+ mutationFn: async (formData) => {
+ try {
+ return await createSubmission(toValue(groupId)!, toValue(formData));
+ } catch (error) {
+ if (
+ error instanceof FetchError &&
+ Array.isArray(error.body?.detail) &&
+ error.body.detail.length > 0 &&
+ (error.body.detail[0].type === "mandatory" ||
+ error.body.detail[0].type === "forbidden")
+ ) {
+ // file requirements were not met,
+ // server returned files that did not meet the requirements
+ const unmetArr = error.body.detail.map((r) => ({
+ requirement: {
+ mandatory: r.type === "mandatory",
+ value: r.requirement,
+ },
+ files: r.files,
+ }));
+ throw new UnmetRequirementsError(
+ "Submission did not meet project requirements",
+ unmetArr
+ );
+ } else {
+ throw error;
+ }
+ }
+ },
onSettled: () => {
queryClient.invalidateQueries({ queryKey: SUBMISSIONS_QUERY_KEY(toValue(groupId)!) });
},
onError: (error) => {
console.error("Submission creation failed", error);
- alert("Could not create submission. Please try again.");
+
+ if (!(error instanceof UnmetRequirementsError)) {
+ alert("Could not create submission. Please try again.");
+ }
},
});
}
diff --git a/frontend/src/services/index.ts b/frontend/src/services/index.ts
index c90c197e..e1a04a52 100644
--- a/frontend/src/services/index.ts
+++ b/frontend/src/services/index.ts
@@ -13,11 +13,20 @@ const defaultOptions: FetchOptions = {
toJson: true,
};
+export class FetchError extends Error {
+ body: any;
+
+ constructor(message?: string, body?: any, ...params: any[]) {
+ super(message, ...params);
+ this.body = body;
+ }
+}
+
/**
* Fetch data from the API
* @param endpoint API endpoint
* @param requestOptions Custom request options
- * @param omitContentType Omit the Content-Type header
+ * @param options Custom fetch options
* @returns Response from the API
*/
export async function authorized_fetch(
@@ -29,7 +38,7 @@ export async function authorized_fetch(
const { token, isLoggedIn } = storeToRefs(useAuthStore());
const { refresh } = useAuthStore();
if (!isLoggedIn) {
- throw new Error("User is not logged in");
+ throw new FetchError("User is not logged in");
}
const { "Content-Type": contentType, ...strippedHeaders } = {
Authorization: `${token.value!.token_type} ${token.value!.token}`,
@@ -47,10 +56,10 @@ export async function authorized_fetch(
});
if (response.status === 401) {
await refresh();
- throw new Error("Not authenticated");
+ throw new FetchError("Not authenticated", response.status);
} else if (!response.ok) {
const error = await response.json();
- throw new Error(error.detail, { cause: error.detail });
+ throw new FetchError(error.detail, error);
}
return mergedOptions.toJson ? response.json() : response;
}