From 74d773afd21eb7fed927dfcae349b21988de4887 Mon Sep 17 00:00:00 2001 From: Selwyn Ang Date: Thu, 26 Sep 2024 14:45:59 +0800 Subject: [PATCH 1/3] Fix duplicate handling in update questions backend --- question-service/app/crud/questions.py | 6 +++++- question-service/app/routers/questions.py | 8 ++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/question-service/app/crud/questions.py b/question-service/app/crud/questions.py index 05c7a8d92d..82f114eeba 100644 --- a/question-service/app/crud/questions.py +++ b/question-service/app/crud/questions.py @@ -39,7 +39,11 @@ async def delete_question(question_id: str): async def update_question_by_id(question_id: str, question_data: UpdateQuestionModel): existing_question = await question_collection.find_one({"_id": ObjectId(question_id)}) if existing_question is None: - return None + return {"error": "does not exist"} + + duplicate_question = await question_collection.find_one({"title": question_data.title}) + if duplicate_question: + return {"error": "duplicate"} updated_data = {"$set": question_data.model_dump(exclude_unset=True)} if not updated_data["$set"]: diff --git a/question-service/app/routers/questions.py b/question-service/app/routers/questions.py index e9da7f0628..736effdfb8 100644 --- a/question-service/app/routers/questions.py +++ b/question-service/app/routers/questions.py @@ -46,7 +46,11 @@ async def delete(question_id: str): @router.put("/{question_id}", response_description="Update question with specified id", response_model=QuestionModel) async def update_question(question_id: str, question_data: UpdateQuestionModel): updated_question = await update_question_by_id(question_id, question_data) - if updated_question is None: - raise HTTPException(status_code=404, detail="Question with this id does not exist.") + + if "error" in updated_question: + if updated_question["error"] == "does not exist": + raise HTTPException(status_code=404, detail="Question with this id does not exist.") + if updated_question["error"] == "duplicate": + raise HTTPException(status_code=409, detail="Question with this title already exists.") return updated_question From efc487f515eaba99d3e25bb7dad6b836a54ac8e9 Mon Sep 17 00:00:00 2001 From: Selwyn Ang Date: Thu, 26 Sep 2024 16:08:40 +0800 Subject: [PATCH 2/3] Update fix --- question-service/app/crud/questions.py | 23 ++++++++++++++--------- question-service/app/routers/questions.py | 10 +++++----- 2 files changed, 19 insertions(+), 14 deletions(-) diff --git a/question-service/app/crud/questions.py b/question-service/app/crud/questions.py index 82f114eeba..3e9c524d75 100644 --- a/question-service/app/crud/questions.py +++ b/question-service/app/crud/questions.py @@ -38,16 +38,21 @@ async def delete_question(question_id: str): async def update_question_by_id(question_id: str, question_data: UpdateQuestionModel): existing_question = await question_collection.find_one({"_id": ObjectId(question_id)}) - if existing_question is None: - return {"error": "does not exist"} - duplicate_question = await question_collection.find_one({"title": question_data.title}) - if duplicate_question: - return {"error": "duplicate"} + if existing_question is None: + return None + + update_data = question_data.model_dump(exclude_unset=True) + + # Check if the new title already exists and belongs to another question + if "title" in update_data and update_data["title"] != existing_question["title"]: + existing_title = await question_collection.find_one({"title": update_data["title"]}) + if existing_title and str(existing_title["_id"]) != question_id: + return "duplicate_title" - updated_data = {"$set": question_data.model_dump(exclude_unset=True)} - if not updated_data["$set"]: + if not update_data: return existing_question - - await question_collection.update_one({"_id": ObjectId(question_id)}, updated_data) + + await question_collection.update_one({"_id": ObjectId(question_id)}, {"$set": update_data}) return await question_collection.find_one({"_id": ObjectId(question_id)}) + diff --git a/question-service/app/routers/questions.py b/question-service/app/routers/questions.py index 736effdfb8..e6ad552466 100644 --- a/question-service/app/routers/questions.py +++ b/question-service/app/routers/questions.py @@ -47,10 +47,10 @@ async def delete(question_id: str): async def update_question(question_id: str, question_data: UpdateQuestionModel): updated_question = await update_question_by_id(question_id, question_data) - if "error" in updated_question: - if updated_question["error"] == "does not exist": - raise HTTPException(status_code=404, detail="Question with this id does not exist.") - if updated_question["error"] == "duplicate": - raise HTTPException(status_code=409, detail="Question with this title already exists.") + if updated_question is None: + raise HTTPException(status_code=404, detail="Question with this id does not exist.") + + if updated_question == "duplicate_title": + raise HTTPException(status_code=409, detail="A question with this title already exists.") return updated_question From be87f9705632fd8c2ae9b2143312c8f81a4cd62a Mon Sep 17 00:00:00 2001 From: Selwyn Ang Date: Thu, 26 Sep 2024 17:10:06 +0800 Subject: [PATCH 3/3] Implement Create new question frontend (#126) * Implement create new question button * Add create new question page and generalise question form * Implement handleCreate for creating new questions * Restore original use-toast hook * Clean up * Run prettier fix * Add success variant of toast * Remove authorisation statements * Remove log statement * Remove excess statement * Resolve merge conflict * Align upload json and create new question buttons on the same level --- frontend/app/app/questions/create/page.tsx | 5 ++ .../components/questions/question-create.tsx | 64 +++++++++++++++++++ .../components/questions/question-form.tsx | 60 ++++++++++------- .../questions/question-view-edit.tsx | 10 ++- .../questions/questions-listing.tsx | 60 +++++++++++------ 5 files changed, 156 insertions(+), 43 deletions(-) create mode 100644 frontend/app/app/questions/create/page.tsx create mode 100644 frontend/components/questions/question-create.tsx diff --git a/frontend/app/app/questions/create/page.tsx b/frontend/app/app/questions/create/page.tsx new file mode 100644 index 0000000000..383b26daee --- /dev/null +++ b/frontend/app/app/questions/create/page.tsx @@ -0,0 +1,5 @@ +import QuestionCreate from "@/components/questions/question-create"; + +export default function QuestionCreatePage() { + return ; +} diff --git a/frontend/components/questions/question-create.tsx b/frontend/components/questions/question-create.tsx new file mode 100644 index 0000000000..e7baee5f69 --- /dev/null +++ b/frontend/components/questions/question-create.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Question } from "@/lib/schemas/question-schema"; +import QuestionForm from "@/components/questions/question-form"; +import { useAuth } from "@/app/auth/auth-context"; +import { useRouter } from "next/navigation"; +import { useToast } from "@/components/hooks/use-toast"; + +export default function QuestionCreate() { + const auth = useAuth(); + const router = useRouter(); + const { toast } = useToast(); + + const handleCreate = async (newQuestion: Question) => { + try { + const response = await fetch("http://localhost:8000/questions", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + title: newQuestion.title, + description: newQuestion.description, + category: newQuestion.category, + complexity: newQuestion.complexity, + }), + }); + + if (!response.ok) { + if (response.status == 409) { + throw new Error("A question with this title already exists."); + } + } + + toast({ + title: "Success", + description: "Question created successfully!", + variant: "success", + duration: 3000, + }); + + router.push(`/app/questions/`); + } catch (err) { + toast({ + title: "An error occured!", + description: + err instanceof Error ? err.message : "An unknown error occurred", + variant: "destructive", + duration: 5000, + }); + } + }; + + return ( +
+

Create a New Question

+ +
+ ); +} diff --git a/frontend/components/questions/question-form.tsx b/frontend/components/questions/question-form.tsx index 63d02c8199..b8719515ff 100644 --- a/frontend/components/questions/question-form.tsx +++ b/frontend/components/questions/question-form.tsx @@ -22,31 +22,49 @@ import { Label } from "@/components/ui/label"; import { AutosizeTextarea } from "../ui/autosize-textarea"; interface QuestionFormProps { - data: Question | undefined; + initialData?: Question; isAdmin: boolean | undefined; - handleSubmit?: (question: Question) => void; + handleSubmit: (question: Question) => void; + submitButtonText: string; } const QuestionForm: React.FC = ({ ...props }) => { - const [question, setQuestion] = useState(); + const [question, setQuestion] = useState( + props.initialData || { + id: "", + title: "", + category: "", + complexity: "easy", + description: "", + } + ); useEffect(() => { - setQuestion(props.data); - }, [props.data]); + if (props.initialData) { + setQuestion(props.initialData); + } + }, [props.initialData]); + + const onSubmit = (e: React.FormEvent) => { + e.preventDefault(); + props.handleSubmit(question); + }; return ( -
+ - question && setQuestion({ ...question, title: e.target.value }) + setQuestion({ ...question, title: e.target.value }) } disabled={!props.isAdmin} + required /> @@ -54,13 +72,14 @@ const QuestionForm: React.FC = ({ ...props }) => {
- question && setQuestion({ ...question, category: e.target.value }) } disabled={!props.isAdmin} + required />
@@ -68,9 +87,9 @@ const QuestionForm: React.FC = ({ ...props }) => {
{props.isAdmin ? ( ) : ( - question && setQuestion({ ...question, complexity: e.target.value }) } disabled={!props.isAdmin} @@ -98,26 +117,21 @@ const QuestionForm: React.FC = ({ ...props }) => {
- question && setQuestion({ ...question, description: e.target.value }) } disabled={!props.isAdmin} + required />
{props.isAdmin && ( - + )} diff --git a/frontend/components/questions/question-view-edit.tsx b/frontend/components/questions/question-view-edit.tsx index 42997ea5c0..e4a40c176d 100644 --- a/frontend/components/questions/question-view-edit.tsx +++ b/frontend/components/questions/question-view-edit.tsx @@ -5,6 +5,7 @@ import useSWR from "swr"; import QuestionForm from "@/components/questions/question-form"; import { useAuth } from "@/app/auth/auth-context"; import { useEffect, useState } from "react"; +import LoadingScreen from "@/components/common/loading-screen"; const fetcher = async (url: string): Promise => { const token = localStorage.getItem("jwtToken"); @@ -35,7 +36,7 @@ export default function QuestionViewEdit({ }) { const auth = useAuth(); - const { data } = useSWR( + const { data, isLoading } = useSWR( `http://localhost:8000/questions/${questionId}`, fetcher ); @@ -51,13 +52,18 @@ export default function QuestionViewEdit({ question; }; + if (isLoading) { + return ; + } + return (

{question?.title}

); diff --git a/frontend/components/questions/questions-listing.tsx b/frontend/components/questions/questions-listing.tsx index 579539ebb5..b5ef71caf9 100644 --- a/frontend/components/questions/questions-listing.tsx +++ b/frontend/components/questions/questions-listing.tsx @@ -9,7 +9,7 @@ import LoadingScreen from "@/components/common/loading-screen"; import DeleteQuestionModal from "@/components/questions/delete-question-modal"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { Upload } from "lucide-react"; +import { PlusIcon, Upload } from "lucide-react"; import { useToast } from "@/components/hooks/use-toast"; import { CreateQuestion, @@ -61,6 +61,25 @@ export default function QuestionListing() { router.push(`/app/questions/${question.id}`); }; + const handleCreateNewQuestion = () => { + router.push(`/app/questions/create`); + }; + + const createNewQuestion = () => { + return ( +
+ +
+ ); + } + const handleFileSelect = (event: ChangeEvent) => { const file = event.target.files?.[0]; if (file) { @@ -176,22 +195,27 @@ export default function QuestionListing() {

Question Listing

{auth?.user?.isAdmin && ( -
- - +
+
+ + +
+
+ {createNewQuestion()} +
)}
); -} +} \ No newline at end of file