Skip to content

Commit

Permalink
Merge branch 'main' into feature/user-service/admin-edit-user
Browse files Browse the repository at this point in the history
  • Loading branch information
jq1836 committed Sep 26, 2024
2 parents 428fef4 + b8c0ae6 commit 4b9679f
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 48 deletions.
5 changes: 5 additions & 0 deletions frontend/app/app/questions/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import QuestionCreate from "@/components/questions/question-create";

export default function QuestionCreatePage() {
return <QuestionCreate />;
}
64 changes: 64 additions & 0 deletions frontend/components/questions/question-create.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Create a New Question</h1>
<QuestionForm
isAdmin={auth?.user?.isAdmin}
handleSubmit={handleCreate}
submitButtonText="Create Question"
/>
</div>
);
}
60 changes: 37 additions & 23 deletions frontend/components/questions/question-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,55 +22,74 @@ 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<QuestionFormProps> = ({ ...props }) => {
const [question, setQuestion] = useState<Question>();
const [question, setQuestion] = useState<Question>(
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 (
<form>
<form onSubmit={onSubmit}>
<Card>
<CardHeader>
<CardTitle className="m-4">
<Label>Title</Label>
<Input
value={question?.title}
id="title"
value={question.title}
className="mt-2"
onChange={(e) =>
question && setQuestion({ ...question, title: e.target.value })
setQuestion({ ...question, title: e.target.value })
}
disabled={!props.isAdmin}
required
/>
</CardTitle>
</CardHeader>
<CardContent>
<div className="m-4">
<Label>Category</Label>
<Input
value={question?.category}
id="category"
value={question.category}
className="mt-2"
onChange={(e) =>
question &&
setQuestion({ ...question, category: e.target.value })
}
disabled={!props.isAdmin}
required
/>
</div>
<div className="m-4">
<Label>Complexity</Label>
<div className="mt-2">
{props.isAdmin ? (
<Select
value={question?.complexity}
value={question.complexity}
onValueChange={(e) =>
question && setQuestion({ ...question, complexity: e })
setQuestion({ ...question, complexity: e })
}
disabled={!props.isAdmin}
>
Expand All @@ -85,9 +104,9 @@ const QuestionForm: React.FC<QuestionFormProps> = ({ ...props }) => {
</Select>
) : (
<Input
value={question?.complexity}
id="complexity"
value={question.complexity}
onChange={(e) =>
question &&
setQuestion({ ...question, complexity: e.target.value })
}
disabled={!props.isAdmin}
Expand All @@ -98,26 +117,21 @@ const QuestionForm: React.FC<QuestionFormProps> = ({ ...props }) => {
<div className="m-4">
<Label>Description</Label>
<AutosizeTextarea
value={question?.description}
id="description"
value={question.description}
className="mt-2"
minHeight={200}
onChange={(e) =>
question &&
setQuestion({ ...question, description: e.target.value })
}
disabled={!props.isAdmin}
required
/>
</div>
</CardContent>
<CardFooter>
{props.isAdmin && (
<Button
onClick={() =>
question && props.handleSubmit && props.handleSubmit(question)
}
>
Save Changes
</Button>
<Button type="submit">{props.submitButtonText}</Button>
)}
</CardFooter>
</Card>
Expand Down
10 changes: 8 additions & 2 deletions frontend/components/questions/question-view-edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Question> => {
const token = localStorage.getItem("jwtToken");
Expand Down Expand Up @@ -35,7 +36,7 @@ export default function QuestionViewEdit({
}) {
const auth = useAuth();

const { data } = useSWR(
const { data, isLoading } = useSWR(
`http://localhost:8000/questions/${questionId}`,
fetcher
);
Expand All @@ -51,13 +52,18 @@ export default function QuestionViewEdit({
question;
};

if (isLoading) {
return <LoadingScreen />;
}

return (
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">{question?.title}</h1>
<QuestionForm
data={question}
initialData={question}
isAdmin={auth?.user?.isAdmin}
handleSubmit={handleEdit}
submitButtonText="Save Changes"
/>
</div>
);
Expand Down
60 changes: 42 additions & 18 deletions frontend/components/questions/questions-listing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -61,6 +61,25 @@ export default function QuestionListing() {
router.push(`/app/questions/${question.id}`);
};

const handleCreateNewQuestion = () => {
router.push(`/app/questions/create`);
};

const createNewQuestion = () => {
return (
<div className="flex justify-end">
<Button
variant="outline"
className="ml-2"
onClick={() => handleCreateNewQuestion()}
>
<PlusIcon className="mr-2" />
Create New Question
</Button>
</div>
);
}

const handleFileSelect = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
Expand Down Expand Up @@ -176,22 +195,27 @@ export default function QuestionListing() {
<div className="container mx-auto p-4">
<h1 className="text-2xl font-bold mb-4">Question Listing</h1>
{auth?.user?.isAdmin && (
<div className="mb-4">
<input
type="file"
accept=".json"
onChange={handleFileSelect}
style={{ display: "none" }}
id="batch-upload-input"
/>
<label htmlFor="batch-upload-input">
<Button variant="outline" asChild>
<span>
<Upload className="mr-2 h-4 w-4" /> Upload questions from JSON
file
</span>
</Button>
</label>
<div className="flex justify-between mb-4">
<div>
<input
type="file"
accept=".json"
onChange={handleFileSelect}
style={{ display: "none" }}
id="batch-upload-input"
/>
<label htmlFor="batch-upload-input">
<Button variant="outline" asChild>
<span>
<Upload className="mr-2 h-4 w-4" /> Upload questions from JSON
file
</span>
</Button>
</label>
</div>
<div>
{createNewQuestion()}
</div>
</div>
)}
<QuestionTable
Expand All @@ -208,4 +232,4 @@ export default function QuestionListing() {
/>
</div>
);
}
}
18 changes: 13 additions & 5 deletions question-service/app/crud/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,22 @@ 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

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)})

async def batch_create_questions(questions: List[CreateQuestionModel]):
Expand All @@ -62,4 +70,4 @@ async def batch_create_questions(questions: List[CreateQuestionModel]):

result = await question_collection.insert_many(new_questions)

return {"message": f"{len(result.inserted_ids)} questions added successfully."}
return {"message": f"{len(result.inserted_ids)} questions added successfully."}
4 changes: 4 additions & 0 deletions question-service/app/routers/questions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,13 @@ 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 updated_question == "duplicate_title":
raise HTTPException(status_code=409, detail="A question with this title already exists.")

return updated_question


Expand Down

0 comments on commit 4b9679f

Please sign in to comment.