From f6b0838c992cd1805ea047673ed9cfaefad8f2d2 Mon Sep 17 00:00:00 2001 From: Prajwal Moharana <78167757+pmoharana-cmd@users.noreply.github.com> Date: Sat, 4 Jan 2025 23:28:12 -0500 Subject: [PATCH] Api create moharana (#53) * Add create user endpoint and update model/entity * Add next route for adding user and add functionality for user table * Connect create item to backend and add associated frontend routes --- backend/api/resource.py | 25 ++++++++++++--- backend/api/user.py | 13 ++++++-- backend/entities/user_entity.py | 4 +-- backend/models/user_model.py | 8 ++--- backend/services/user.py | 35 +++++++++----------- compass/app/admin/page.tsx | 8 +++-- compass/app/api/resource/create/route.ts | 37 ++++++++++++++++++++++ compass/app/api/service/create/route.ts | 37 ++++++++++++++++++++++ compass/app/api/user/all/route.ts | 2 +- compass/app/api/user/create/route.ts | 37 ++++++++++++++++++++++ compass/app/resource/page.tsx | 9 +++++- compass/app/service/page.tsx | 9 +++++- compass/components/Drawer/CreateDrawer.tsx | 3 +- compass/components/Drawer/Drawer.tsx | 3 +- compass/components/Table/ResourceTable.tsx | 14 +++++--- compass/components/Table/ServiceTable.tsx | 28 +++++++++------- compass/components/Table/Table.tsx | 26 +++++++++++++-- compass/components/Table/UserTable.tsx | 16 ++++++---- compass/components/TagsInput/Index.tsx | 9 ++---- compass/components/TagsInput/Tag.tsx | 1 + 20 files changed, 252 insertions(+), 72 deletions(-) create mode 100644 compass/app/api/resource/create/route.ts create mode 100644 compass/app/api/service/create/route.ts create mode 100644 compass/app/api/user/create/route.ts diff --git a/backend/api/resource.py b/backend/api/resource.py index 97d25af..ff449e2 100644 --- a/backend/api/resource.py +++ b/backend/api/resource.py @@ -19,7 +19,10 @@ openapi_tags = { # TODO: Create custom exceptions @api.post("", response_model=Resource, tags=["Resource"]) def create( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + resource: Resource, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.create(subject, resource) @@ -27,14 +30,20 @@ def create( @api.get("", response_model=List[Resource], tags=["Resource"]) def get_all( - uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.get_resource_by_user(subject) + @api.get("/{name}", response_model=Resource, tags=["Resource"]) def get_by_name( - name:str, uuid:str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + name: str, + uuid: str, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.get_resource_by_name(name, subject) @@ -42,7 +51,10 @@ def get_by_name( @api.put("", response_model=Resource, tags=["Resource"]) def update( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + resource: Resource, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) return resource_svc.update(subject, resource) @@ -50,7 +62,10 @@ def update( @api.delete("", response_model=None, tags=["Resource"]) def delete( - uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() + uuid: str, + resource: Resource, + user_svc: UserService = Depends(), + resource_svc: ResourceService = Depends(), ): subject = user_svc.get_user_by_uuid(uuid) resource_svc.delete(subject, resource) diff --git a/backend/api/user.py b/backend/api/user.py index b7ec372..9066d71 100644 --- a/backend/api/user.py +++ b/backend/api/user.py @@ -16,8 +16,8 @@ openapi_tags = { # TODO: Enable authorization by passing user uuid to API # TODO: Create custom exceptions @api.get("/all", response_model=List[User], tags=["Users"]) -def get_all(user_id: str, user_svc: UserService = Depends()): - subject = user_svc.get_user_by_uuid(user_id) +def get_all(uuid: str, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) if subject.role != UserTypeEnum.ADMIN: raise Exception(f"Insufficient permissions for user {subject.uuid}") @@ -28,3 +28,12 @@ def get_all(user_id: str, user_svc: UserService = Depends()): @api.get("/{user_id}", response_model=User, tags=["Users"]) def get_by_uuid(user_id: str, user_svc: UserService = Depends()): return user_svc.get_user_by_uuid(user_id) + + +@api.post("/", response_model=User, tags=["Users"]) +def create_user(uuid: str, user: User, user_svc: UserService = Depends()): + subject = user_svc.get_user_by_uuid(uuid) + if subject.role != UserTypeEnum.ADMIN: + raise Exception(f"Insufficient permissions for user {subject.uuid}") + + return user_svc.create(user) diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index ca66ee9..3d4cdd7 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -38,8 +38,8 @@ class UserEntity(EntityBase): program: Mapped[list[ProgramTypeEnum]] = mapped_column( ARRAY(Enum(ProgramTypeEnum)), nullable=False ) - experience: Mapped[int] = mapped_column(Integer, nullable=False) - group: Mapped[str] = mapped_column(String(50)) + experience: Mapped[int] = mapped_column(Integer, nullable=True) + group: Mapped[str] = mapped_column(String(50), nullable=True) uuid: Mapped[str] = mapped_column(String, nullable=True) @classmethod diff --git a/backend/models/user_model.py b/backend/models/user_model.py index e2c25da..60c3007 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -10,9 +10,9 @@ class User(BaseModel): id: int | None = None username: str = Field(..., description="The username of the user") email: str = Field(..., description="The e-mail of the user") - experience: int = Field(..., description="Years of Experience of the User") - group: str - program: List[ProgramTypeEnum] - role: UserTypeEnum + experience: int | None = Field(None, description="Years of Experience of the User") + group: str | None = Field(None, description="The group of the user") + program: List[ProgramTypeEnum] | None = None + role: UserTypeEnum | None = None created_at: Optional[datetime] = datetime.now() uuid: str | None = None diff --git a/backend/services/user.py b/backend/services/user.py index 360db01..59bc5c4 100644 --- a/backend/services/user.py +++ b/backend/services/user.py @@ -61,22 +61,21 @@ class UserService: """ try: - if (user.id != None): + if user.id != None: user = self.get_user_by_id(user.id) + else: + user_entity = UserEntity.from_model(user) + # add new user to table + self._session.add(user_entity) + self._session.commit() except: - # if does not exist, create new object - user_entity = UserEntity.from_model(user) + raise Exception(f"Failed to create user") - # add new user to table - self._session.add(user_entity) - self._session.commit() - finally: - # return added object - return user - - def delete(self, user: User) -> None: + return user + + def delete(self, user: User) -> None: """ - Delete a user + Delete a user Args: the user to delete @@ -86,25 +85,23 @@ class UserService: if obj is None: raise Exception(f"No matching user found") - + self._session.delete(obj) self._session.commit() - - - def update(self, user: User) -> User: + def update(self, user: User) -> User: """ Updates a user Args: User to be updated Returns: The updated User - """ + """ obj = self._session.get(UserEntity, user.id) if obj is None: raise Exception(f"No matching user found") - + obj.username = user.username obj.role = user.role obj.email = user.email @@ -115,5 +112,3 @@ class UserService: self._session.commit() return obj.to_model() - - diff --git a/compass/app/admin/page.tsx b/compass/app/admin/page.tsx index abe2a8c..4d4781d 100644 --- a/compass/app/admin/page.tsx +++ b/compass/app/admin/page.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; export default function Page() { const [users, setUsers] = useState([]); + const [uuid, setUuid] = useState(""); useEffect(() => { async function getUser() { @@ -22,8 +23,10 @@ export default function Page() { return; } + setUuid(data.user.id); + const userListData = await fetch( - `${process.env.NEXT_PUBLIC_HOST}/api/user/all?uuid=${data.user.id}` + `/api/user/all?uuid=${data.user.id}` ); const users: User[] = await userListData.json(); @@ -38,7 +41,8 @@ export default function Page() {
{/* icon + title */} }> - + {/* TODO: REPLACE UUID WITH HTTP BEARER TOKEN */} +
); diff --git a/compass/app/api/resource/create/route.ts b/compass/app/api/resource/create/route.ts new file mode 100644 index 0000000..8698b16 --- /dev/null +++ b/compass/app/api/resource/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/resource`; + + try { + const resourceData = await request.json(); + + console.log("RESOURCE DATA", JSON.stringify(resourceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(resourceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdResource: User = await response.json(); + return NextResponse.json(createdResource, { status: response.status }); + } catch (error) { + console.error("Error creating resource:", error); + return NextResponse.json( + { error: "Failed to create resource" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/service/create/route.ts b/compass/app/api/service/create/route.ts new file mode 100644 index 0000000..957911d --- /dev/null +++ b/compass/app/api/service/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/service`; + + try { + const serviceData = await request.json(); + + console.log("SERVICE DATA", JSON.stringify(serviceData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(serviceData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdService: User = await response.json(); + return NextResponse.json(createdService, { status: response.status }); + } catch (error) { + console.error("Error creating service:", error); + return NextResponse.json( + { error: "Failed to create service" }, + { status: 500 } + ); + } +} diff --git a/compass/app/api/user/all/route.ts b/compass/app/api/user/all/route.ts index 5f7259a..0f072eb 100644 --- a/compass/app/api/user/all/route.ts +++ b/compass/app/api/user/all/route.ts @@ -9,7 +9,7 @@ export async function GET(request: Request) { const { searchParams } = new URL(request.url); const uuid = searchParams.get("uuid"); - const data = await fetch(`${apiEndpoint}?user_id=${uuid}`); + const data = await fetch(`${apiEndpoint}?uuid=${uuid}`); const userData: User[] = await data.json(); // TODO: Remove make every user visible diff --git a/compass/app/api/user/create/route.ts b/compass/app/api/user/create/route.ts new file mode 100644 index 0000000..d7ea091 --- /dev/null +++ b/compass/app/api/user/create/route.ts @@ -0,0 +1,37 @@ +import { NextResponse } from "next/server"; +import User from "@/utils/models/User"; + +export async function POST(request: Request) { + const apiEndpoint = `${process.env.NEXT_PUBLIC_API_HOST}/api/user`; + + try { + const userData = await request.json(); + + console.log("USER DATA", JSON.stringify(userData)); + + const { searchParams } = new URL(request.url); + const uuid = searchParams.get("uuid"); + + // Send the POST request to the backend + const response = await fetch(`${apiEndpoint}?uuid=${uuid}`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(userData), + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const createdUser: User = await response.json(); + return NextResponse.json(createdUser, { status: response.status }); + } catch (error) { + console.error("Error creating user:", error); + return NextResponse.json( + { error: "Failed to create user" }, + { status: 500 } + ); + } +} diff --git a/compass/app/resource/page.tsx b/compass/app/resource/page.tsx index 7cbd2c1..6a886d3 100644 --- a/compass/app/resource/page.tsx +++ b/compass/app/resource/page.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; export default function Page() { const [resources, setResources] = useState([]); + const [uuid, setUuid] = useState(""); useEffect(() => { async function getResources() { @@ -22,6 +23,8 @@ export default function Page() { return; } + setUuid(data.user.id); + const userListData = await fetch( `${process.env.NEXT_PUBLIC_HOST}/api/resource/all?uuid=${data.user.id}` ); @@ -38,7 +41,11 @@ export default function Page() {
{/* icon + title */} }> - +
); diff --git a/compass/app/service/page.tsx b/compass/app/service/page.tsx index efe6337..3366430 100644 --- a/compass/app/service/page.tsx +++ b/compass/app/service/page.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; export default function Page() { const [services, setServices] = useState([]); + const [uuid, setUuid] = useState(""); useEffect(() => { async function getServices() { @@ -22,6 +23,8 @@ export default function Page() { return; } + setUuid(data.user.id); + const serviceListData = await fetch( `${process.env.NEXT_PUBLIC_HOST}/api/service/all?uuid=${data.user.id}` ); @@ -37,7 +40,11 @@ export default function Page() {
{/* icon + title */} }> - +
); diff --git a/compass/components/Drawer/CreateDrawer.tsx b/compass/components/Drawer/CreateDrawer.tsx index d31173d..d8511b7 100644 --- a/compass/components/Drawer/CreateDrawer.tsx +++ b/compass/components/Drawer/CreateDrawer.tsx @@ -26,8 +26,6 @@ const CreateDrawer: FunctionComponent = ({ e: React.ChangeEvent ) => { const { name, value } = e.target; - console.log(newItemContent); - console.log(Object.keys(newItemContent).length); setNewItemContent((prev: any) => ({ ...prev, [name]: value, @@ -45,6 +43,7 @@ const CreateDrawer: FunctionComponent = ({ const handleCreate = () => { if (onCreate(newItemContent)) { + console.log("newItemContent", newItemContent); setNewItemContent({}); setIsOpen(false); } diff --git a/compass/components/Drawer/Drawer.tsx b/compass/components/Drawer/Drawer.tsx index 97d6c15..a572036 100644 --- a/compass/components/Drawer/Drawer.tsx +++ b/compass/components/Drawer/Drawer.tsx @@ -79,13 +79,14 @@ const Drawer: FunctionComponent = ({ return row; }); }); + + console.log("Send API request to update row content"); } setIsOpen(!isOpen); if (isFull) { setIsFull(!isFull); } - console.log("Send API request to update row content"); }; const toggleDrawerFullScreen = () => setIsFull(!isFull); diff --git a/compass/components/Table/ResourceTable.tsx b/compass/components/Table/ResourceTable.tsx index e2b5a50..a85ffdd 100644 --- a/compass/components/Table/ResourceTable.tsx +++ b/compass/components/Table/ResourceTable.tsx @@ -18,6 +18,7 @@ import { Tag } from "../TagsInput/Tag"; type ResourceTableProps = { data: Resource[]; setData: Dispatch>; + uuid: string; }; /** @@ -25,13 +26,17 @@ type ResourceTableProps = { * @param props.data Stateful list of resources to be displayed by the table * @param props.setData State setter for the list of resources */ -export default function ResourceTable({ data, setData }: ResourceTableProps) { +export default function ResourceTable({ + data, + setData, + uuid, +}: ResourceTableProps) { const columnHelper = createColumnHelper(); const [programPresets, setProgramPresets] = useState([ - "domestic", - "community", - "economic", + "DOMESTIC", + "COMMUNITY", + "ECONOMIC", ]); const resourceDetails: Details[] = [ @@ -137,6 +142,7 @@ export default function ResourceTable({ data, setData }: ResourceTableProps) { setData={setData} columns={columns} details={resourceDetails} + createEndpoint={`/api/resource/create?uuid=${uuid}`} /> ); } diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx index 02bd1be..a4cfad3 100644 --- a/compass/components/Table/ServiceTable.tsx +++ b/compass/components/Table/ServiceTable.tsx @@ -16,6 +16,7 @@ import { Tag } from "../TagsInput/Tag"; type ServiceTableProps = { data: Service[]; setData: Dispatch>; + uuid: string; }; /** @@ -23,23 +24,27 @@ type ServiceTableProps = { * @param props.data Stateful list of services to be displayed by the table * @param props.setData State setter for the list of services */ -export default function ServiceTable({ data, setData }: ServiceTableProps) { +export default function ServiceTable({ + data, + setData, + uuid, +}: ServiceTableProps) { const columnHelper = createColumnHelper(); const [programPresets, setProgramPresets] = useState([ - "domestic", - "community", - "economic", + "DOMESTIC", + "COMMUNITY", + "ECONOMIC", ]); const [requirementPresets, setRequirementPresets] = useState([ - "anonymous", - "confidential", - "referral required", - "safety assessment", - "intake required", - "income eligibility", - "initial assessment", + "ANONYMOUS", + "CONFIDENTIAL", + "REFERRAL REQUIRED", + "SAFETY ASSESSMENT", + "INTAKE REQUIRED", + "INCOME ELIGIBILITY", + "INITIAL ASSESSMENT", ]); const serviceDetails: Details[] = [ @@ -165,6 +170,7 @@ export default function ServiceTable({ data, setData }: ServiceTableProps) { setData={setData} columns={columns} details={serviceDetails} + createEndpoint={`/api/service/create?uuid=${uuid}`} /> ); } diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index 9a9433b..2b208fc 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -27,6 +27,7 @@ type TableProps = { setData: Dispatch>; columns: ColumnDef[]; details: Details[]; + createEndpoint: string; }; /** Validates that all required fields in a new item have values */ @@ -76,9 +77,22 @@ export default function Table({ setData, columns, details, + createEndpoint, }: TableProps) { const columnHelper = createColumnHelper(); + const createRow = async (newItem: any) => { + const response = await fetch(createEndpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(newItem), + }); + + return response; + }; + // /** Sorting function based on visibility */ // const visibilitySort = (a: T, b: T) => // a.visible === b.visible ? 0 : a.visible ? -1 : 1; @@ -224,8 +238,16 @@ export default function Table({ return false; } - newItem.visible = true; - setData((prev) => [...prev, newItem]); + createRow(newItem).then((response) => { + if (response.ok) { + newItem.visible = true; + setData((prev) => [ + ...prev, + newItem, + ]); + } + }); + return true; }} /> diff --git a/compass/components/Table/UserTable.tsx b/compass/components/Table/UserTable.tsx index 58b018a..6914e46 100644 --- a/compass/components/Table/UserTable.tsx +++ b/compass/components/Table/UserTable.tsx @@ -19,6 +19,7 @@ import { Tag } from "../TagsInput/Tag"; type UserTableProps = { data: User[]; setData: Dispatch>; + uuid: string; }; /** @@ -26,19 +27,19 @@ type UserTableProps = { * @param props.data Stateful list of users to be displayed by the table * @param props.setData State setter for the list of users */ -export default function UserTable({ data, setData }: UserTableProps) { +export default function UserTable({ data, setData, uuid }: UserTableProps) { const columnHelper = createColumnHelper(); const [rolePresets, setRolePresets] = useState([ - "admin", - "volunteer", - "employee", + "ADMIN", + "VOLUNTEER", + "EMPLOYEE", ]); const [programPresets, setProgramPresets] = useState([ - "domestic", - "community", - "economic", + "DOMESTIC", + "COMMUNITY", + "ECONOMIC", ]); const userDetails: Details[] = [ @@ -144,6 +145,7 @@ export default function UserTable({ data, setData }: UserTableProps) { setData={setData} columns={columns} details={userDetails} + createEndpoint={`/api/user/create?uuid=${uuid}`} /> ); } diff --git a/compass/components/TagsInput/Index.tsx b/compass/components/TagsInput/Index.tsx index e02a661..fd54caf 100644 --- a/compass/components/TagsInput/Index.tsx +++ b/compass/components/TagsInput/Index.tsx @@ -23,12 +23,8 @@ const TagsInput: React.FC = ({ const [cellSelected, setCellSelected] = useState(false); // TODO: Add tags to the database and remove the presetValue and lowercasing - const [tags, setTags] = useState>( - new Set(presetValue.map((tag) => tag.toLowerCase())) - ); - const [options, setOptions] = useState>( - new Set(presetOptions.map((option) => option.toLowerCase())) - ); + const [tags, setTags] = useState>(new Set(presetValue)); + const [options, setOptions] = useState>(new Set(presetOptions)); const [filteredOptions, setFilteredOptions] = useState>( new Set(presetOptions) ); @@ -44,7 +40,6 @@ const TagsInput: React.FC = ({ }; const handleOutsideClick = (event: MouseEvent) => { - console.log(dropdown.current); if ( dropdown.current && !dropdown.current.contains(event.target as Node) diff --git a/compass/components/TagsInput/Tag.tsx b/compass/components/TagsInput/Tag.tsx index 044e484..665572e 100644 --- a/compass/components/TagsInput/Tag.tsx +++ b/compass/components/TagsInput/Tag.tsx @@ -11,6 +11,7 @@ export const Tag = ({ children, handleDelete, active = false }: TagProps) => { return ( {children} {active && handleDelete && (