Skip to content

Commit b16bdce

Browse files
committed
add error handling for rename to existing name degree
1 parent c133fd3 commit b16bdce

File tree

3 files changed

+101
-5
lines changed

3 files changed

+101
-5
lines changed

backend/degree/views.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,20 @@ def retrieve(self, request, *args, **kwargs):
8484
serializer = self.get_serializer(degree_plan)
8585
return Response(serializer.data, status=status.HTTP_200_OK)
8686

87+
def update(self, request, *args, **kwargs):
88+
name = request.data.get("name")
89+
instance = self.get_object()
90+
if (
91+
name
92+
and instance.name != name
93+
and DegreePlan.objects.filter(name=name, person=request.user).exists()
94+
):
95+
return Response(
96+
{"warning": f"A degree plan with name {name} already exists."},
97+
status=status.HTTP_409_CONFLICT,
98+
)
99+
return super().update(request, *args, **kwargs)
100+
87101
def create(self, request, *args, **kwargs):
88102
name = request.data.get("name")
89103
if name is None:

frontend/degree-plan/components/FourYearPlan/DegreeModal.tsx

Lines changed: 86 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import type {
77
SchoolOption,
88
} from "@/types";
99
import React, { useState, useEffect } from "react";
10-
import { deleteFetcher, postFetcher, useSWRCrud } from "@/hooks/swrcrud";
10+
import { deleteFetcher, postFetcher, useSWRCrud, getCsrf, normalizeFinalSlash } from "@/hooks/swrcrud";
1111
import useSWR, { useSWRConfig } from "swr";
1212
import ModalContainer from "../common/ModalContainer";
1313
import Select from "react-select";
1414
import { schoolOptions } from "@/components/OnboardingPanels/SharedComponents";
15+
import { ErrorText } from "@/components/OnboardingPanels/SharedComponents";
16+
import { assertValueType } from "@/types";
1517

1618
export type ModalKey =
1719
| "plan-create"
@@ -157,7 +159,6 @@ const ModalInterior = ({
157159
}: ModalInteriorProps) => {
158160
const {
159161
create: createDegreeplan,
160-
update: updateDegreeplan,
161162
remove: deleteDegreeplan,
162163
} = useSWRCrud<DegreePlan>("/api/degree/degreeplans");
163164

@@ -222,6 +223,85 @@ const ModalInterior = ({
222223
await mutate(`/api/degree/degreeplans/${degreeplanId}`); // use updated degree plan returned
223224
};
224225

226+
227+
228+
// Update degree plan handling error case where degree plan of same name already exists.
229+
const [sameNameError, setSameNameError] = useState(false);
230+
231+
const updateDegreeplanWithErrorHandling = async (updatedData: Partial<DegreePlan>, id: number | string | null) => {
232+
if (!id) return;
233+
234+
const key = normalizeFinalSlash(`/api/degree/degreeplans/${id}`);
235+
const res = await fetch(key, {
236+
credentials: "include",
237+
mode: "same-origin",
238+
method: "PATCH",
239+
headers: {
240+
"Content-Type": "application/json",
241+
"X-CSRFToken": getCsrf(),
242+
"Accept": "application/json",
243+
} as HeadersInit,
244+
body: JSON.stringify({ name: name }),
245+
});
246+
247+
if (res.ok) {
248+
const updated = await res.json();
249+
250+
// Handle mutation
251+
// Code adapted from swrcrud.ts
252+
const idKey = "id" as keyof DegreePlan;
253+
254+
mutate(key, updated, {
255+
optimisticData: (data?: DegreePlan) => {
256+
const optimistic = {...data, ...updatedData} as DegreePlan;
257+
assertValueType(optimistic, idKey, id)
258+
optimistic.id = Number(id); // does this work?
259+
return ({ id, ...data, ...updatedData} as DegreePlan)
260+
},
261+
revalidate: false,
262+
throwOnError: false
263+
})
264+
265+
const endpoint = "/api/degree/degreeplans";
266+
mutate(endpoint, updated, {
267+
optimisticData: (list?: Array<DegreePlan>) => {
268+
if (!list) return [];
269+
const index = list.findIndex((item: DegreePlan) => String(item[idKey]) === id);
270+
if (index === -1) {
271+
mutate(endpoint) // trigger revalidation
272+
return list;
273+
}
274+
list.splice(index, 1, {...list[index], ...updatedData});
275+
return list;
276+
},
277+
populateCache: (updated: DegreePlan, list?: Array<DegreePlan>) => {
278+
if (!list) return [];
279+
if (!updated) return list;
280+
const index = list.findIndex((item: DegreePlan) => item[idKey] === updated[idKey]);
281+
if (index === -1) {
282+
console.warn("swrcrud: update: updated element not found in list view");
283+
mutate(endpoint); // trigger revalidation
284+
return list;
285+
}
286+
list.splice(index, 1, updated);
287+
return list
288+
},
289+
revalidate: false,
290+
throwOnError: false
291+
})
292+
293+
close(); // only close if update is successful
294+
} else if (res.status === 409) {
295+
setSameNameError(true);
296+
297+
setTimeout(() => {
298+
setSameNameError(false);
299+
}, 5000);
300+
} else {
301+
throw new Error(await res.text());
302+
}
303+
};
304+
225305
switch (modalKey) {
226306
case "plan-create":
227307
return (
@@ -262,18 +342,20 @@ const ModalInterior = ({
262342
"id" in modalObject &&
263343
"name" in modalObject
264344
) {
265-
updateDegreeplan({ name }, modalObject.id);
345+
updateDegreeplanWithErrorHandling({name: name}, modalObject.id);
266346
if (modalObject.id == activeDegreePlan?.id) {
267347
let newNameDegPlan = modalObject;
268348
newNameDegPlan.name = name;
269349
setActiveDegreeplan(newNameDegPlan);
270350
}
351+
} else {
352+
close();
271353
}
272-
close();
273354
}}
274355
>
275356
Rename
276357
</ModalButton>
358+
{sameNameError && <ErrorText style={{ color: "red" }}>A degree plan with this name already exists.</ErrorText>}
277359
</ModalInteriorWrapper>
278360
);
279361
case "plan-remove":

frontend/degree-plan/hooks/swrcrud.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ export const patchFetcher = baseFetcher({ method: "PATCH" })
6565
export const putFetcher = baseFetcher({ method: "PUT" })
6666
export const deleteFetcher = baseFetcher({ method: "DELETE" }, false); // expect no response from delete requests
6767

68-
const normalizeFinalSlash = (resource: string) => {
68+
export const normalizeFinalSlash = (resource: string) => {
6969
if (!resource.endsWith("/")) resource += "/";
7070
return resource
7171
}

0 commit comments

Comments
 (0)