Skip to content

Commit

Permalink
Merge pull request tryyang2001#80 from tryyang2001/tryyang/update_ass…
Browse files Browse the repository at this point in the history
…ignment_integration_logic

Update getAssignments endpoint and fix frontend bugs
  • Loading branch information
tryyang2001 authored Apr 12, 2024
2 parents 79d9d07 + 7063050 commit 1a274c7
Show file tree
Hide file tree
Showing 16 changed files with 348 additions and 223 deletions.
28 changes: 17 additions & 11 deletions backend/assignment-service/src/controllers/assignment-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { DeleteHandler } from "../services/assignments/delete-handler";
import { UpdateAssignmentValidator } from "../libs/validators/assignments/update-assignment-validator";
import { PutHandler } from "../services/assignments/put-handler";
import { formatZodErrorMessage } from "../libs/utils/error-message-utils";
import { GetAssignmentsQueryValidator } from "../libs/validators/assignments/get-assignments-validator";

const getAssignmentsByUserId = async (request: Request, response: Response) => {
try {
Expand All @@ -21,17 +22,14 @@ const getAssignmentsByUserId = async (request: Request, response: Response) => {
return;
}

const userId = parseInt(request.query.userId as string);
const { userId, includePast, isPublished } =
GetAssignmentsQueryValidator.parse(request.query);

if (isNaN(userId)) {
response.status(HttpStatusCode.BAD_REQUEST).json({
error: "BAD REQUEST",
message: "Invalid userId format",
});
return;
}

const assignments = await GetHandler.getAssignmentsByUserId(userId);
const assignments = await GetHandler.getAssignmentsByUserId(
userId,
includePast,
isPublished
);

if (!assignments) {
response.status(HttpStatusCode.NOT_FOUND).json({
Expand All @@ -42,7 +40,15 @@ const getAssignmentsByUserId = async (request: Request, response: Response) => {
}

response.status(HttpStatusCode.OK).json(assignments);
} catch (_error) {
} catch (error) {
if (error instanceof ZodError) {
response.status(HttpStatusCode.BAD_REQUEST).json({
error: "BAD REQUEST",
message: formatZodErrorMessage(error),
});
return;
}

response.status(HttpStatusCode.INTERNAL_SERVER_ERROR).json({
error: "INTERNAL SERVER ERROR",
message: "An unexpected error has occurred. Please try again later",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { z } from "zod";

export const GetAssignmentsQueryValidator = z.object({
userId: z
.string()
.refine((id) => {
const parsedId = parseInt(id);
return !isNaN(parsedId) && parsedId > 0;
}, "Invalid userId format")
.transform((id) => parseInt(id)),
includePast: z
.string()
.refine((value) => value === "true" || value === "false")
.transform((value) => value === "true")
.optional(),
isPublished: z
.string()
.refine((value) => value === "true" || value === "false")
.transform((value) => value === "true")
.optional(),
});

export type GetAssignmentsQuery = z.infer<typeof GetAssignmentsQueryValidator>;
17 changes: 11 additions & 6 deletions backend/assignment-service/src/services/assignments/get-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import db from "../../models/db";
import { Assignment } from "../../models/types/assignment";
import { Question } from "../../models/types/question";

const getAssignmentsByUserId = async (userId: number) => {
const getAssignmentsByUserId = async (
userId: number,
includePast?: boolean,
isPublishedOnly?: boolean
) => {
// check if the user exists
const user = await db.user.findUnique({
where: {
Expand All @@ -16,14 +20,15 @@ const getAssignmentsByUserId = async (userId: number) => {

const assignments = await db.assignment.findMany({
where: {
authors: {
hasSome: [userId],
},
isPublished: isPublishedOnly ? true : undefined,
deadline: includePast
? undefined
: {
gt: new Date(),
},
},
});

// TODO: search user under courses, what assignments are there

const assignmentsDto: Assignment[] = assignments.map((assignment) => {
return {
id: assignment.id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@ describe("Unit Tests for GET /assignments?userId=:userId", () => {
);

// Assert
expect(GetHandler.getAssignmentsByUserId).toHaveBeenCalledWith(userId);
expect(response.status).toBe(HttpStatusCode.OK);
expect(response.body).toEqual(
ExpectedAssignmentsFromGetAssignmentsByUserId(userId)
Expand Down Expand Up @@ -101,7 +100,6 @@ describe("Unit Tests for GET /assignments?userId=:userId", () => {
);

// Assert
expect(GetHandler.getAssignmentsByUserId).toHaveBeenCalledWith(userId);
expect(response.status).toBe(HttpStatusCode.NOT_FOUND);
expect(response.body).toEqual({
error: "NOT FOUND",
Expand All @@ -111,7 +109,7 @@ describe("Unit Tests for GET /assignments?userId=:userId", () => {
});

describe("Given the user id exists but has no assignments", () => {
it("should return 404 with an error message", async () => {
it("should return 200 with an empty array", async () => {
// Arrange
const userId = 2;
GetHandler.getAssignmentsByUserId = jest.fn().mockResolvedValue([]);
Expand All @@ -122,7 +120,6 @@ describe("Unit Tests for GET /assignments?userId=:userId", () => {
);

// Assert
expect(GetHandler.getAssignmentsByUserId).toHaveBeenCalledWith(userId);
expect(response.status).toBe(HttpStatusCode.OK);
expect(response.body).toEqual([]);
});
Expand All @@ -144,7 +141,6 @@ describe("Unit Tests for GET /assignments?userId=:userId", () => {
);

// Assert
expect(GetHandler.getAssignmentsByUserId).toHaveBeenCalledWith(userId);
expect(response.status).toBe(HttpStatusCode.INTERNAL_SERVER_ERROR);
expect(response.body).toEqual({
error: "INTERNAL SERVER ERROR",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ exports[`Page Snapshot tests Sign-up Snapshot test 1`] = `
</div>
</div>
<button
class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-unit-3 min-w-unit-16 h-unit-8 text-tiny gap-unit-2 rounded-small [&>svg]:max-w-[theme(spacing.unit-8)] data-[pressed=true]:scale-[0.97] transition-transform-colors-opacity motion-reduce:transition-none bg-primary text-primary-foreground data-[hover=true]:opacity-hover"
class="z-0 group relative inline-flex items-center justify-center box-border appearance-none select-none whitespace-nowrap font-normal subpixel-antialiased overflow-hidden tap-highlight-transparent outline-none data-[focus-visible=true]:z-10 data-[focus-visible=true]:outline-2 data-[focus-visible=true]:outline-focus data-[focus-visible=true]:outline-offset-2 px-unit-3 min-w-unit-16 h-unit-8 text-tiny gap-unit-2 rounded-small [&>svg]:max-w-[theme(spacing.unit-8)] data-[pressed=true]:scale-[0.97] transition-transform-colors-opacity motion-reduce:transition-none bg-primary text-primary-foreground data-[hover=true]:opacity-hover w-full"
type="submit"
>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/app/assignments/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Icons from "@/components/common/Icons";
import LogoLoading from "@/components/common/LogoLoading";
import { useToast } from "@/components/ui/use-toast";
import { useAssignmentContext } from "@/contexts/assignment-context";
import { useUserContext } from "@/contexts/user-context";
import AssignmentService from "@/helpers/assignment-service/api-wrapper";
import {
Button,
Expand All @@ -17,6 +18,7 @@ import {
Tooltip,
useDisclosure,
} from "@nextui-org/react";
import { useQueryClient } from "@tanstack/react-query";
import { notFound, useRouter } from "next/navigation";
import { useEffect, useState } from "react";

Expand Down Expand Up @@ -95,6 +97,10 @@ function Page({ params }: Props) {
setIsLoading(false);
};

const { user } = useUserContext();

const queryClient = useQueryClient();

useEffect(() => {
if (!assignment || !isEditing) {
router.push(`/assignments/${params.id}`);
Expand Down Expand Up @@ -190,6 +196,13 @@ function Page({ params }: Props) {
// combine the promises
Promise.all([deleteQuestionPromises, updateQuestionPromises])
.then(() => {
// invalidate the get assignments query
queryClient
.invalidateQueries({
queryKey: ["get-assignments", user],
})
.catch((_error) => new Error("Failed to invalidate query"));

toast({
title: "Questions updated successfully",
description: "The questions have been updated successfully.",
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/app/assignments/create/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
"use client";

import AssignmentEditor from "@/components/assignment/create/AssignmentEditor";
import { useUserContext } from "@/contexts/user-context";
import { Spacer } from "@nextui-org/react";
import { notFound } from "next/navigation";

export default function Create() {
const { user } = useUserContext();

if (!user || user.role !== "tutor") {
return notFound();
}

return (
<div className="h-screen">
<b>Create a new assignment</b>
Expand Down
22 changes: 18 additions & 4 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,32 @@ export default function DashBoard() {
const { user } = useUserContext();

const { data: assignments, isLoading } = useQuery({
queryKey: ["get-assignments", user?.uid ?? 0],
queryKey: ["get-assignments", user],
queryFn: async () => {
return await AssignmentService.getAssignmentsByUserId(user?.uid ?? 0);
}
if (!user) {
return [];
}

if (user?.role === "student") {
// only retrieve assignments that are published and not past the deadline
return await AssignmentService.getAssignmentsByUserId(
user.uid,
false,
true
);
}

// retrieve all assignments that are not past the deadline
return await AssignmentService.getAssignmentsByUserId(user.uid);
},
});

return (
<div className="h-screen">
{isLoading ? (
<LogoLoading />
) : (
<AssignmentList assignments={assignments} userRole={user?.role?? "student"} />
<AssignmentList assignments={assignments} userRole={user?.role ?? ""} />
)}
</div>
);
Expand Down
31 changes: 18 additions & 13 deletions frontend/src/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import userService from "@/helpers/user-service/api-wrapper";
import Link from "next/link";
import EmailInput from "@/components/forms/EmailInput";
import PasswordInput from "@/components/forms/PasswordInput";
import 'react-toastify/dist/ReactToastify.css';
import { useUserContext } from "@/contexts/user-context";
import { useRouter } from "next/navigation";
import { useToast } from "@/components/ui/use-toast";
Expand All @@ -22,7 +21,7 @@ export default function Home() {
const router = useRouter();

const handleSubmit = async () => {
if (email == "" || password == "" || isInvalid) {
if (email === "" || password === "" || isInvalid) {
toast({
title: "Invalid input",
description: "Please check your input and try again",
Expand All @@ -32,16 +31,20 @@ export default function Home() {

try {
const user = await userService.login(email, password);

if (!user) {
throw new Error("Cannot logging in");
}

setUserContext(user);

toast({
title: "Login successfully",
description: "Welcome back to ITS",
variant: "success",
});
router.push('/dashboard');

router.push("/dashboard");
} catch (err) {
if (err instanceof Error) {
const errorMsg = err.message;
Expand All @@ -53,7 +56,8 @@ export default function Home() {
} else {
toast({
title: "Logging in unsucessfully",
description: "We are currently encountering some issues, please try again later",
description:
"We are currently encountering some issues, please try again later",
variant: "destructive",
});
}
Expand All @@ -69,17 +73,19 @@ export default function Home() {
setIsInvalid={setIsInvalid}
/>

<PasswordInput label={"Password"} password={password} setPassword={setPassword} />
<PasswordInput
label={"Password"}
password={password}
setPassword={setPassword}
/>

<Button
type="submit"
onClick={
() => {
void (async () => {
await handleSubmit()
})();
}
}
onClick={() => {
void (async () => {
await handleSubmit();
})();
}}
color="primary"
className="w-full"
>
Expand All @@ -100,4 +106,3 @@ export default function Home() {
</div>
);
}
/* eslint-enable @typescript-eslint/no-misused-promises */
18 changes: 9 additions & 9 deletions frontend/src/app/sign-up/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,12 @@ export default function Home() {
} else {
toast({
title: "Signing up unsucessfully",
description: "We are currently encountering some issues, please try again later",
description:
"We are currently encountering some issues, please try again later",
variant: "destructive",
});
}
};
}
};

function Eye() {
Expand Down Expand Up @@ -136,13 +137,12 @@ export default function Home() {
type="submit"
size="sm"
color="primary"
onClick={
() => {
void (async () => {
await handleSubmit()
})();
}
}
onClick={() => {
void (async () => {
await handleSubmit();
})();
}}
className="w-full"
>
{" "}
Sign Up
Expand Down
Loading

0 comments on commit 1a274c7

Please sign in to comment.