diff --git a/backend/api/resource.py b/backend/api/resource.py index 98dcaae..97d25af 100644 --- a/backend/api/resource.py +++ b/backend/api/resource.py @@ -1,26 +1,56 @@ -from fastapi import APIRouter, Depends -from ..services import ResourceService, UserService -from ..models.resource_model import Resource - -from typing import List - -api = APIRouter(prefix="/api/resource") - -openapi_tags = { - "name": "Resource", - "description": "Resource search and related operations.", -} - - -# TODO: Add security using HTTP Bearer Tokens -# TODO: Enable authorization by passing user uuid to API -# TODO: Create custom exceptions -@api.get("", response_model=List[Resource], tags=["Resource"]) -def get_all( - user_id: str, - resource_svc: ResourceService = Depends(), - user_svc: UserService = Depends(), -): - subject = user_svc.get_user_by_uuid(user_id) - - return resource_svc.get_resource_by_user(subject) +from fastapi import APIRouter, Depends + +from backend.models.user_model import User +from ..services import ResourceService, UserService +from ..models.resource_model import Resource + +from typing import List + +api = APIRouter(prefix="/api/resource") + +openapi_tags = { + "name": "Resource", + "description": "Resource search and related operations.", +} + + +# TODO: Add security using HTTP Bearer Tokens +# TODO: Enable authorization by passing user uuid to API +# 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() +): + subject = user_svc.get_user_by_uuid(uuid) + return resource_svc.create(subject, resource) + + +@api.get("", response_model=List[Resource], tags=["Resource"]) +def get_all( + 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() +): + subject = user_svc.get_user_by_uuid(uuid) + return resource_svc.get_resource_by_name(name, subject) + + +@api.put("", response_model=Resource, tags=["Resource"]) +def update( + 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) + + +@api.delete("", response_model=None, tags=["Resource"]) +def delete( + 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/service.py b/backend/api/service.py index 97508a9..bd3c4dc 100644 --- a/backend/api/service.py +++ b/backend/api/service.py @@ -1,26 +1,54 @@ -from fastapi import APIRouter, Depends -from ..services import ServiceService, UserService -from ..models.service_model import Service - -from typing import List - -api = APIRouter(prefix="/api/service") - -openapi_tags = { - "name": "Service", - "description": "Service search and related operations.", -} - - -# TODO: Add security using HTTP Bearer Tokens -# TODO: Enable authorization by passing user uuid to API -# TODO: Create custom exceptions -@api.get("", response_model=List[Service], tags=["Service"]) -def get_all( - user_id: str, - service_svc: ServiceService = Depends(), - user_svc: UserService = Depends(), -): - subject = user_svc.get_user_by_uuid(user_id) - - return service_svc.get_service_by_user(subject) +from fastapi import APIRouter, Depends + +from backend.models.user_model import User +from ..services import ServiceService, UserService +from ..models.service_model import Service + +from typing import List + +api = APIRouter(prefix="/api/service") + +openapi_tags = { + "name": "Service", + "description": "Service search and related operations.", +} + + +# TODO: Add security using HTTP Bearer Tokens +# TODO: Enable authorization by passing user uuid to API +# TODO: Create custom exceptions +@api.post("", response_model=Service, tags=["Service"]) +def create( + uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() +): + subject = user_svc.get_user_by_uuid(uuid) + return service_svc.create(subject, service) + + +@api.get("", response_model=List[Service], tags=["Service"]) +def get_all( + uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() +): + subject = user_svc.get_user_by_uuid(uuid) + return service_svc.get_service_by_user(subject) + +@api.get("/{name}", response_model=Service, tags=["Service"]) +def get_by_name( + name: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() +): + subject = user_svc.get_user_by_uuid(uuid) + return service_svc.get_service_by_name(name, subject) + +@api.put("", response_model=Service, tags=["Service"]) +def update( + uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() +): + subject = user_svc.get_user_by_uuid(uuid) + return service_svc.update(subject, service) + +@api.delete("", response_model=None, tags=["Service"]) +def delete( + uuid: str, service: Service, user_svc: UserService = Depends(), service_svc: ServiceService = Depends() +): + subject = user_svc.get_user_by_uuid(uuid) + service_svc.delete(subject, service) diff --git a/backend/api/tag.py b/backend/api/tag.py new file mode 100644 index 0000000..36e7e4a --- /dev/null +++ b/backend/api/tag.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends + +from backend.models.tag_model import Tag +from backend.models.user_model import User +from backend.services.tag import TagService +from ..services import ResourceService, UserService +from ..models.resource_model import Resource + +from typing import List + +api = APIRouter(prefix="/api/tag") + +openapi_tags = { + "name": "Tag", + "description": "Tag CRUD operations.", +} + + +# TODO: Add security using HTTP Bearer Tokens +# TODO: Enable authorization by passing user uuid to API +# TODO: Create custom exceptions +@api.post("", response_model=Tag, tags=["Tag"]) +def create( + subject: User, + tag: Tag, + tag_service: TagService=Depends() +): + return tag_service.create(subject, tag) + +@api.get("", response_model=List[Tag], tags=["Tag"]) +def get_all( + subject: User, + tag_svc: TagService=Depends() +): + return tag_svc.get_all() + +@api.put("", response_model=Tag, tags=["Tag"]) +def update( + subject: User, + tag: Tag, + tag_svc: TagService=Depends() +): + return tag_svc.delete(subject, tag) + +@api.delete("", response_model=None, tags=["Tag"]) +def delete( + subject: User, + tag: Tag, + tag_svc: TagService=Depends() +): + tag_svc.delete(subject, tag) diff --git a/backend/api/test_routes.md b/backend/api/test_routes.md new file mode 100644 index 0000000..64528ba --- /dev/null +++ b/backend/api/test_routes.md @@ -0,0 +1,147 @@ +# Synopsis +Collection of sample curl requests for api routes. + +# Resources +## Get All +Given an admin UUID, gets all of the resources from ResourceEntity. +``` +curl -X 'GET' \ + 'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' +``` + +## Get by Name +Given the name of a resource and an admin UUID, gets a resource from ResourceEntity by name. +``` +curl -X 'GET' \ + 'http://127.0.0.1:8000/api/resource/Financial%20Empowerment%20Center?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' +``` + +## Create +Given an admin UUID and a new resource object, adds a resource to ResourceEntity. +``` +curl -X 'POST' \ + 'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "name": "algorithms and analysis textbook", + "summary": "textbook written by kevin sun for c550", + "link": "kevinsun.org", + "program": "DOMESTIC", + "created_at": "2024-11-04T20:07:31.875166" +}' +``` + +## Update +Given an admin UUID and a modified resource object, updates the resource with a matching ID if it exists. +``` +curl -X 'PUT' \ + 'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "name": "algorithms and analysis textbook", + "summary": "textbook written by the goat himself, kevin sun, for c550", + "link": "kevinsun.org", + "program": "DOMESTIC", + "created_at": "2024-11-04T20:07:31.875166" +}' +``` + +## Delete +Given an admin UUID and a resource object, deletes the resource with a matching ID if it exists. +``` +curl -X 'DELETE' \ + 'http://127.0.0.1:8000/api/resource?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "name": "algorithms and analysis textbook", + "summary": "textbook written by the goat himself, kevin sun, for c550", + "link": "kevinsun.org", + "program": "DOMESTIC", + "created_at": "2024-11-04T20:07:31.875166" +}' +``` + +# Services +## Get All +Given an admin UUID, gets all of the services from ServiceEntity. +``` +curl -X 'GET' \ + 'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' +``` + +## Get by Name +Given the name of a service and an admin UUID, gets a service from ServiceEntity by name. +``` +curl -X 'GET' \ + 'http://127.0.0.1:8000/api/service/Shelter%20Placement?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' +``` + +## Create +Given an admin UUID and a new service object, adds a service to ServiceEntity. +``` +curl -X 'POST' \ + 'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "created_at": "2024-11-04T20:07:31.890412", + "name": "c550 tutoring", + "status": "open", + "summary": "tutoring for kevin sun'\''s c550 class", + "requirements": [ + "must be in c550" + ], + "program": "COMMUNITY" +}' +``` + +## Update +Given an admin UUID and a modified service object, updates the service with a matching ID if it exists. +``` +curl -X 'PUT' \ + 'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "created_at": "2024-11-04T20:07:31.890412", + "name": "c550 tutoring", + "status": "closed", + "summary": "tutoring for kevin sun'\''s c550 class", + "requirements": [ + "must be in c550" + ], + "program": "COMMUNITY" +}' +``` + +## Delete +Given an admin UUID and a service object, deletes the service with a matching ID if it exists. +``` +curl -X 'DELETE' \ + 'http://127.0.0.1:8000/api/service?uuid=acc6e112-d296-4739-a80c-b89b2933e50b' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d '{ + "id": 25, + "created_at": "2024-11-04T20:07:31.890412", + "name": "c550 tutoring", + "status": "closed", + "summary": "tutoring for kevin sun'\''s c550 class", + "requirements": [ + "must be in c550" + ], + "program": "COMMUNITY" +}' +``` \ No newline at end of file diff --git a/backend/entities/resource_entity.py b/backend/entities/resource_entity.py index 299decb..208929b 100644 --- a/backend/entities/resource_entity.py +++ b/backend/entities/resource_entity.py @@ -1,67 +1,67 @@ -""" Defines the table for storing resources """ - -# Import our mapped SQL types from SQLAlchemy -from sqlalchemy import Integer, String, DateTime, Enum - -# Import mapping capabilities from the SQLAlchemy ORM -from sqlalchemy.orm import Mapped, mapped_column, relationship - -# Import the EntityBase that we are extending -from .entity_base import EntityBase - -# Import datetime for created_at type -from datetime import datetime - -# Import self for to model -from typing import Self -from backend.entities.program_enum import Program_Enum -from ..models.resource_model import Resource - - -class ResourceEntity(EntityBase): - - # set table name - __tablename__ = "resource" - - # set fields - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - name: Mapped[str] = mapped_column(String(64), nullable=False) - summary: Mapped[str] = mapped_column(String(100), nullable=False) - link: Mapped[str] = mapped_column(String, nullable=False) - program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) - # relationships - resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( - back_populates="resource", cascade="all,delete" - ) - - @classmethod - def from_model(cls, model: Resource) -> Self: - """ - Create a UserEntity from a User model. - - Args: - model (User): The model to create the entity from. - - Returns: - Self: The entity (not yet persisted). - """ - - return cls( - id=model.id, - created_at=model.created_at, - name=model.name, - summary=model.summary, - link=model.link, - program=model.program, - ) - - def to_model(self) -> Resource: - return Resource( - id=self.id, - created_at=self.created_at, - name=self.name, - summary=self.summary, - link=self.link, - program=self.program, - ) +""" Defines the table for storing resources """ + +# Import our mapped SQL types from SQLAlchemy +from sqlalchemy import Integer, String, DateTime, Enum + +# Import mapping capabilities from the SQLAlchemy ORM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +# Import the EntityBase that we are extending +from .entity_base import EntityBase + +# Import datetime for created_at type +from datetime import datetime + +# Import self for to model +from typing import Self +from backend.entities.program_enum import Program_Enum +from ..models.resource_model import Resource + + +class ResourceEntity(EntityBase): + + # set table name + __tablename__ = "resource" + + # set fields + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + name: Mapped[str] = mapped_column(String(64), nullable=False) + summary: Mapped[str] = mapped_column(String(100), nullable=False) + link: Mapped[str] = mapped_column(String, nullable=False) + program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) + # relationships + resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( + back_populates="resource", cascade="all,delete" + ) + + @classmethod + def from_model(cls, model: Resource) -> Self: + """ + Create a UserEntity from a User model. + + Args: + model (User): The model to create the entity from. + + Returns: + Self: The entity (not yet persisted). + """ + + return cls( + id=model.id, + created_at=model.created_at, + name=model.name, + summary=model.summary, + link=model.link, + program=model.program, + ) + + def to_model(self) -> Resource: + return Resource( + id=self.id, + created_at=self.created_at, + name=self.name, + summary=self.summary, + link=self.link, + program=self.program, + ) \ No newline at end of file diff --git a/backend/entities/resource_tag_entity.py b/backend/entities/resource_tag_entity.py index e6d863b..53a9323 100644 --- a/backend/entities/resource_tag_entity.py +++ b/backend/entities/resource_tag_entity.py @@ -1,46 +1,46 @@ -""" Defines the table for resource tags """ - -# Import our mapped SQL types from SQLAlchemy -from sqlalchemy import ForeignKey, Integer, String, DateTime - -# Import mapping capabilities from the SQLAlchemy ORM -from sqlalchemy.orm import Mapped, mapped_column, relationship - -# Import the EntityBase that we are extending -from .entity_base import EntityBase - -# Import datetime for created_at type -from datetime import datetime - -# Import self for to model -from typing import Self - - -class ResourceTagEntity(EntityBase): - - # set table name to user in the database - __tablename__ = "resource_tag" - - # set fields or 'columns' for the user table - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id")) - tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) - - # relationships - resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags") - tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags") - - # @classmethod - # def from_model (cls, model: resource_tag_model) -> Self: - # return cls ( - # id = model.id, - # resourceId = model.resourceId, - # tagId = model.tagId, - # ) - - # def to_model (self) -> resource_tag_model: - # return user_model( - # id = self.id, - # resourceId = self.resourceId, - # tagId = self.tagId, - # ) +""" Defines the table for resource tags """ + +# Import our mapped SQL types from SQLAlchemy +from sqlalchemy import ForeignKey, Integer, String, DateTime + +# Import mapping capabilities from the SQLAlchemy ORM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +# Import the EntityBase that we are extending +from .entity_base import EntityBase + +# Import datetime for created_at type +from datetime import datetime + +# Import self for to model +from typing import Self + + +class ResourceTagEntity(EntityBase): + + # set table name to user in the database + __tablename__ = "resource_tag" + + # set fields or 'columns' for the user table + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + resourceId: Mapped[int] = mapped_column(ForeignKey("resource.id")) + tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) + + # relationships + resource: Mapped["ResourceEntity"] = relationship(back_populates="resourceTags") + tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags") + + # @classmethod + # def from_model (cls, model: resource_tag_model) -> Self: + # return cls ( + # id = model.id, + # resourceId = model.resourceId, + # tagId = model.tagId, + # ) + + # def to_model (self) -> resource_tag_model: + # return user_model( + # id = self.id, + # resourceId = self.resourceId, + # tagId = self.tagId, + # ) \ No newline at end of file diff --git a/backend/entities/service_entity.py b/backend/entities/service_entity.py index 4f1f9fc..809f5d9 100644 --- a/backend/entities/service_entity.py +++ b/backend/entities/service_entity.py @@ -1,47 +1,47 @@ -""" Defines the table for storing services """ - -# Import our mapped SQL types from SQLAlchemy -from sqlalchemy import Integer, String, DateTime, ARRAY - -# Import mapping capabilities from the SQLAlchemy ORM -from sqlalchemy.orm import Mapped, mapped_column, relationship - -# Import the EntityBase that we are extending -from .entity_base import EntityBase - -# Import datetime for created_at type -from datetime import datetime - -# Import enums for Program -import enum -from sqlalchemy import Enum - -from backend.models.service_model import Service -from typing import Self -from backend.models.enum_for_models import ProgramTypeEnum - -class ServiceEntity(EntityBase): - - # set table name - __tablename__ = "service" - - # set fields - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - name: Mapped[str] = mapped_column(String(32), nullable=False) - status: Mapped[str] = mapped_column(String(32), nullable=False) - summary: Mapped[str] = mapped_column(String(100), nullable=False) - requirements: Mapped[list[str]] = mapped_column(ARRAY(String)) - program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) - - # relationships - serviceTags: Mapped[list["ServiceTagEntity"]] = relationship( - back_populates="service", cascade="all,delete" - ) - - def to_model(self) -> Service: - return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program) - - @classmethod - def from_model(cls, model:Service) -> Self: +""" Defines the table for storing services """ + +# Import our mapped SQL types from SQLAlchemy +from sqlalchemy import Integer, String, DateTime, ARRAY + +# Import mapping capabilities from the SQLAlchemy ORM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +# Import the EntityBase that we are extending +from .entity_base import EntityBase + +# Import datetime for created_at type +from datetime import datetime + +# Import enums for Program +import enum +from sqlalchemy import Enum + +from backend.models.service_model import Service +from typing import Self +from backend.models.enum_for_models import ProgramTypeEnum + +class ServiceEntity(EntityBase): + + # set table name + __tablename__ = "service" + + # set fields + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + name: Mapped[str] = mapped_column(String(32), nullable=False) + status: Mapped[str] = mapped_column(String(32), nullable=False) + summary: Mapped[str] = mapped_column(String(100), nullable=False) + requirements: Mapped[list[str]] = mapped_column(ARRAY(String)) + program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) + + # relationships + serviceTags: Mapped[list["ServiceTagEntity"]] = relationship( + back_populates="service", cascade="all,delete" + ) + + def to_model(self) -> Service: + return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program) + + @classmethod + def from_model(cls, model:Service) -> Self: return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program) \ No newline at end of file diff --git a/backend/entities/service_tag_entity.py b/backend/entities/service_tag_entity.py index 0f05738..6d4e908 100644 --- a/backend/entities/service_tag_entity.py +++ b/backend/entities/service_tag_entity.py @@ -1,25 +1,26 @@ -""" Defines the table for service tags """ - -# Import our mapped SQL types from SQLAlchemy -from sqlalchemy import ForeignKey, Integer - -# Import mapping capabilities from the SQLAlchemy ORM -from sqlalchemy.orm import Mapped, mapped_column, relationship - -# Import the EntityBase that we are extending -from .entity_base import EntityBase - - -class ServiceTagEntity(EntityBase): - - # set table name to user in the database - __tablename__ = "service_tag" - - # set fields or 'columns' for the user table - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - serviceId: Mapped[int] = mapped_column(ForeignKey("service.id")) - tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) - - # relationships - service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") - tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") +""" Defines the table for service tags """ + +# Import our mapped SQL types from SQLAlchemy +from sqlalchemy import ForeignKey, Integer + +# Import mapping capabilities from the SQLAlchemy ORM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +# Import the EntityBase that we are extending +from .entity_base import EntityBase + + +class ServiceTagEntity(EntityBase): + + # set table name to user in the database + __tablename__ = "service_tag" + + # set fields or 'columns' for the user table + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + serviceId: Mapped[int] = mapped_column(ForeignKey("service.id")) + tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) + + # relationships + service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") + tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") + \ No newline at end of file diff --git a/backend/entities/tag_entity.py b/backend/entities/tag_entity.py index 0d1548b..3916798 100644 --- a/backend/entities/tag_entity.py +++ b/backend/entities/tag_entity.py @@ -1,65 +1,64 @@ -""" Defines the table for storing tags """ - -# Import our mapped SQL types from SQLAlchemy -from sqlalchemy import Integer, String, DateTime - -# Import mapping capabilities from the SQLAlchemy ORM -from sqlalchemy.orm import Mapped, mapped_column, relationship - -# Import the EntityBase that we are extending -from .entity_base import EntityBase - -# Import datetime for created_at type -from datetime import datetime - -from ..models.tag_model import Tag - -from typing import Self - -class TagEntity(EntityBase): - - #set table name - __tablename__ = "tag" - - #set fields - id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) - content: Mapped[str] = mapped_column(String(100), nullable=False) - - #relationships - resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") - serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") - - - - @classmethod - def from_model(cls, model: Tag) -> Self: - """ - Create a user entity from model - - Args: model (User): the model to create the entity from - - Returns: - self: The entity - """ - - return cls( - id=model.id, - content=model.id, - ) - - def to_model(self) -> Tag: - """ - Create a user model from entity - - Returns: - User: A User model for API usage - """ - - return Tag( - id=self.id, - content=self.content, - ) - - - +""" Defines the table for storing tags """ + +# Import our mapped SQL types from SQLAlchemy +from sqlalchemy import Integer, String, DateTime + +# Import mapping capabilities from the SQLAlchemy ORM +from sqlalchemy.orm import Mapped, mapped_column, relationship + +# Import the EntityBase that we are extending +from .entity_base import EntityBase + +# Import datetime for created_at type +from datetime import datetime + +from ..models.tag_model import Tag + +from typing import Self + +class TagEntity(EntityBase): + + #set table name + __tablename__ = "tag" + + #set fields + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) + content: Mapped[str] = mapped_column(String(100), nullable=False) + + #relationships + resourceTags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") + serviceTags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag", cascade="all,delete") + + + + @classmethod + def from_model(cls, model: Tag) -> Self: + """ + Create a user entity from model + + Args: model (User): the model to create the entity from + + Returns: + self: The entity + """ + + return cls( + id=model.id, + created_at=model.created_at, + content=model.id, + ) + + def to_model(self) -> Tag: + """ + Create a user model from entity + + Returns: + User: A User model for API usage + """ + + return Tag( + id=self.id, + create_at=self.created_at, + content=self.content, + ) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index d8af3ff..12ecaad 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,34 +1,36 @@ -from fastapi import FastAPI, Request -from fastapi.responses import JSONResponse -from fastapi.middleware.gzip import GZipMiddleware - -from .api import user, health, service, resource - -description = """ -Welcome to the **COMPASS** RESTful Application Programming Interface. -""" - -app = FastAPI( - title="Compass API", - version="0.0.1", - description=description, - openapi_tags=[ - user.openapi_tags, - health.openapi_tags, - service.openapi_tags, - resource.openapi_tags, - ], -) - -app.add_middleware(GZipMiddleware) - -feature_apis = [user, health, service, resource] - -for feature_api in feature_apis: - app.include_router(feature_api.api) - - -# Add application-wide exception handling middleware for commonly encountered API Exceptions -@app.exception_handler(Exception) -def permission_exception_handler(request: Request, e: Exception): - return JSONResponse(status_code=403, content={"message": str(e)}) +from fastapi import FastAPI, Request +from fastapi.responses import JSONResponse +from fastapi.middleware.gzip import GZipMiddleware + + +from .api import user, health, service, resource, tag + +description = """ +Welcome to the **COMPASS** RESTful Application Programming Interface. +""" + +app = FastAPI( + title="Compass API", + version="0.0.1", + description=description, + openapi_tags=[ + user.openapi_tags, + health.openapi_tags, + service.openapi_tags, + resource.openapi_tags, + tag.openapi_tags + ], +) + +app.add_middleware(GZipMiddleware) + +feature_apis = [user, health, service, resource, tag] + +for feature_api in feature_apis: + app.include_router(feature_api.api) + + +# Add application-wide exception handling middleware for commonly encountered API Exceptions +@app.exception_handler(Exception) +def permission_exception_handler(request: Request, e: Exception): + return JSONResponse(status_code=403, content={"message": str(e)}) diff --git a/backend/models/resource_model.py b/backend/models/resource_model.py index 8c9fde0..81e49cc 100644 --- a/backend/models/resource_model.py +++ b/backend/models/resource_model.py @@ -12,4 +12,4 @@ class Resource(BaseModel): summary: str = Field(..., max_length=300, description="The summary of the resource") link: str = Field(..., max_length=150, description="link to the resource") program: ProgramTypeEnum - created_at: Optional[datetime] + created_at: Optional[datetime] = datetime.now() diff --git a/backend/models/service_model.py b/backend/models/service_model.py index 36c336b..9671974 100644 --- a/backend/models/service_model.py +++ b/backend/models/service_model.py @@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum class Service(BaseModel): id: int | None = None - created_at: datetime | None = None + created_at: datetime | None = datetime.now() name: str status: str summary: str diff --git a/backend/models/tag_model.py b/backend/models/tag_model.py index 44dcb02..fee566c 100644 --- a/backend/models/tag_model.py +++ b/backend/models/tag_model.py @@ -10,4 +10,4 @@ class Tag(BaseModel): content: str = Field( ..., max_length=600, description="content associated with the tag" ) - created_at: datetime | None = None + created_at: datetime | None = datetime.now() diff --git a/backend/models/user_model.py b/backend/models/user_model.py index d7c1521..e2c25da 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -14,5 +14,5 @@ class User(BaseModel): group: str program: List[ProgramTypeEnum] role: UserTypeEnum - created_at: Optional[datetime] + created_at: Optional[datetime] = datetime.now() uuid: str | None = None diff --git a/backend/services/exceptions.py b/backend/services/exceptions.py index 069bff1..daa4b9b 100644 --- a/backend/services/exceptions.py +++ b/backend/services/exceptions.py @@ -20,6 +20,8 @@ class UserPermissionException(Exception): class ServiceNotFoundException(Exception): """Exception for when the service being requested is not in the table.""" +class TagNotFoundException(Exception): + """Exception for when the tag being requested is not in the table.""" class ProgramNotAssignedException(Exception): """Exception for when the user does not have correct access for requested services.""" diff --git a/backend/services/resource.py b/backend/services/resource.py index 67959c7..c06af66 100644 --- a/backend/services/resource.py +++ b/backend/services/resource.py @@ -1,12 +1,12 @@ from fastapi import Depends from ..database import db_session from sqlalchemy.orm import Session -from sqlalchemy import select +from sqlalchemy import and_, select from ..models.resource_model import Resource from ..entities.resource_entity import ResourceEntity from ..models.user_model import User, UserTypeEnum -from .exceptions import ResourceNotFoundException +from .exceptions import ProgramNotAssignedException, ResourceNotFoundException class ResourceService: @@ -14,25 +14,40 @@ class ResourceService: def __init__(self, session: Session = Depends(db_session)): self._session = session - def get_resource_by_user(self, subject: User): + def get_resource_by_user(self, subject: User) -> list[Resource]: """Resource method getting all of the resources that a user has access to based on role""" if subject.role != UserTypeEnum.VOLUNTEER: query = select(ResourceEntity) entities = self._session.scalars(query).all() - return [resource.to_model() for resource in entities] else: programs = subject.program resources = [] for program in programs: - query = select(ResourceEntity).filter(ResourceEntity.program == program) - entities = self._session.scalars(query).all() + entities = ( + self._session.query(ResourceEntity) + .where(ResourceEntity.program == program) + .all() + ) for entity in entities: - resources.append(entity) + resources.append(entity.to_model()) + return [resource for resource in resources] - return [resource.to_model() for resource in resources] + def get_resource_by_name(self, name: str, subject: User) -> Resource: + """Get a resource by name.""" + query = select(ResourceEntity).where( + and_( + ResourceEntity.name == name, ResourceEntity.program.in_(subject.program) + ) + ) + entity = self._session.scalars(query).one_or_none() + if entity is None: + raise ResourceNotFoundException( + f"Resource with name: {name} does not exist or program has not been assigned." + ) + return entity.to_model() - def create(self, user: User, resource: Resource) -> Resource: + def create(self, subject: User, resource: Resource) -> Resource: """ Creates a resource based on the input object and adds it to the table if the user has the right permissions. Parameters: @@ -41,43 +56,16 @@ class ResourceService: Returns: Resource: Object added to table """ - if user.role != UserTypeEnum.ADMIN: - raise PermissionError( - "User does not have permission to add resources in this program." + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot update service" ) - resource_entity = ResourceEntity.from_model(resource) self._session.add(resource_entity) self._session.commit() - return resource_entity.to_model() - def get_by_id(self, user: User, id: int) -> Resource: - """ - Gets a resource based on the resource id that the user has access to - Parameters: - user: a valid User model representing the currently logged in User - id: int, the id of the resource - Returns: - Resource - Raises: - ResourceNotFoundException: If no resource is found with id - """ - resource = ( - self._session.query(ResourceEntity) - .filter( - ResourceEntity.id == id, - ResourceEntity.program.in_(user.program), - ) - .one_or_none() - ) - - if resource is None: - raise ResourceNotFoundException(f"No resource found with id: {id}") - - return resource.to_model() - - def update(self, user: User, resource: ResourceEntity) -> Resource: + def update(self, subject: User, resource: Resource) -> Resource: """ Update the resource if the user has access Parameters: @@ -88,28 +76,24 @@ class ResourceService: Raises: ResourceNotFoundException: If no resource is found with the corresponding ID """ - if user.role != UserTypeEnum.ADMIN: - raise PermissionError( - "User does not have permission to update this resource." + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot update service" ) - - obj = self._session.get(ResourceEntity, resource.id) if resource.id else None - - if obj is None: + query = select(ResourceEntity).where(ResourceEntity.id == resource.id) + entity = self._session.scalars(query).one_or_none() + if entity is None: raise ResourceNotFoundException( f"No resource found with matching id: {resource.id}" ) - - obj.name = resource.name - obj.summary = resource.summary - obj.link = resource.link - obj.program = resource.program - + entity.name = resource.name + entity.summary = resource.summary + entity.link = resource.link + entity.program = resource.program self._session.commit() + return entity.to_model() - return obj.to_model() - - def delete(self, user: User, id: int) -> None: + def delete(self, subject: User, resource: Resource) -> None: """ Delete resource based on id that the user has access to Parameters: @@ -118,23 +102,17 @@ class ResourceService: Raises: ResourceNotFoundException: If no resource is found with the corresponding id """ - if user.role != UserTypeEnum.ADMIN: - raise PermissionError( - "User does not have permission to delete this resource." + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot update service" ) - - resource = ( - self._session.query(ResourceEntity) - .filter( - ResourceEntity.id == id, + query = select(ResourceEntity).where(ResourceEntity.id == resource.id) + entity = self._session.scalars(query).one_or_none() + if entity is None: + raise ResourceNotFoundException( + f"No resource found with matching id: {resource.id}" ) - .one_or_none() - ) - - if resource is None: - raise ResourceNotFoundException(f"No resource found with matching id: {id}") - - self._session.delete(resource) + self._session.delete(entity) self._session.commit() def get_by_slug(self, user: User, search_string: str) -> list[Resource]: @@ -150,11 +128,11 @@ class ResourceService: """ query = select(ResourceEntity).where( ResourceEntity.name.ilike(f"%{search_string}%"), - ResourceEntity.program.in_(user.program) + ResourceEntity.program.in_(user.program), ) entities = self._session.scalars(query).all() if not entities: return [] - return [entity.to_model() for entity in entities] \ No newline at end of file + return [entity.to_model() for entity in entities] diff --git a/backend/services/service.py b/backend/services/service.py index f8afe2e..e10309e 100644 --- a/backend/services/service.py +++ b/backend/services/service.py @@ -1,127 +1,101 @@ -from fastapi import Depends - -from ..database import db_session -from sqlalchemy.orm import Session -from sqlalchemy import func, select, and_, func, or_, exists, or_ - -from backend.models.service_model import Service -from backend.models.user_model import User -from backend.entities.service_entity import ServiceEntity -from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum -from backend.services.exceptions import ( - ServiceNotFoundException, - ProgramNotAssignedException, -) - - -class ServiceService: - - def __init__(self, session: Session = Depends(db_session)): - self._session = session - - def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]: - """Service method getting services belonging to a particular program.""" - query = select(ServiceEntity).filter(ServiceEntity.program == program) - entities = self._session.scalars(query) - - return [entity.to_model() for entity in entities] - - def get_service_by_id(self, id: int) -> Service: - """Service method getting services by id.""" - query = select(ServiceEntity).filter(ServiceEntity.id == id) - entity = self._session.scalars(query).one_or_none() - - if entity is None: - raise ServiceNotFoundException(f"Service with id: {id} does not exist") - - return entity.to_model() - - def get_service_by_name(self, name: str) -> Service: - """Service method getting services by id.""" - query = select(ServiceEntity).filter(ServiceEntity.name == name) - entity = self._session.scalars(query).one_or_none() - - if entity is None: - raise ServiceNotFoundException(f"Service with name: {name} does not exist") - - return entity.to_model() - - def get_service_by_user(self, subject: User): - """Service method getting all of the services that a user has access to based on role""" - if subject.role != UserTypeEnum.VOLUNTEER: - query = select(ServiceEntity) - entities = self._session.scalars(query).all() - - return [service.to_model() for service in entities] - else: - programs = subject.program - services = [] - for program in programs: - query = select(ServiceEntity).filter(ServiceEntity.program == program) - entities = self._session.scalars(query).all() - for entity in entities: - services.append(entity) - - return [service.to_model() for service in services] - - def get_all(self, subject: User) -> list[Service]: - """Service method retrieving all of the services in the table.""" - if subject.role == UserTypeEnum.VOLUNTEER: - raise ProgramNotAssignedException( - f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all" - ) - - query = select(ServiceEntity) - entities = self._session.scalars(query).all() - - return [service.to_model() for service in entities] - - def create(self, subject: User, service: Service) -> Service: - """Creates/adds a service to the table.""" - if subject.role != UserTypeEnum.ADMIN: - raise ProgramNotAssignedException( - f"User is not {UserTypeEnum.ADMIN}, cannot create service" - ) - - service_entity = ServiceEntity.from_model(service) - self._session.add(service_entity) - self._session.commit() - return service_entity.to_model() - - def update(self, subject: User, service: Service) -> Service: - """Updates a service if in the table.""" - if subject.role != UserTypeEnum.ADMIN: - raise ProgramNotAssignedException( - f"User is not {UserTypeEnum.ADMIN}, cannot update service" - ) - - service_entity = self._session.get(ServiceEntity, service.id) - - if service_entity is None: - raise ServiceNotFoundException( - "The service you are searching for does not exist." - ) - - service_entity.name = service.name - service_entity.status = service.status - service_entity.summary = service.summary - service_entity.requirements = service.requirements - service_entity.program = service.program - - self._session.commit() - - return service_entity.to_model() - - def delete(self, subject: User, service: Service) -> None: - """Deletes a service from the table.""" - if subject.role != UserTypeEnum.ADMIN: - raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") - service_entity = self._session.get(ServiceEntity, service.id) - - if service_entity is None: - raise ServiceNotFoundException( - "The service you are searching for does not exist." - ) - - self._session.delete(service_entity) - self._session.commit() +from fastapi import Depends + +from ..database import db_session +from sqlalchemy.orm import Session +from sqlalchemy import func, select, and_, func, or_, exists, or_ + +from backend.models.service_model import Service +from backend.models.user_model import User +from backend.entities.service_entity import ServiceEntity +from backend.models.enum_for_models import ProgramTypeEnum, UserTypeEnum +from backend.services.exceptions import ( + ServiceNotFoundException, + ProgramNotAssignedException, +) + + +class ServiceService: + + def __init__(self, session: Session = Depends(db_session)): + self._session = session + + def get_service_by_user(self, subject: User) -> list[Service]: + """Resource method getting all of the resources that a user has access to based on role""" + if subject.role != UserTypeEnum.VOLUNTEER: + query = select(ServiceEntity) + entities = self._session.scalars(query).all() + return [service.to_model() for service in entities] + else: + programs = subject.program + services = [] + for program in programs: + entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all() + for entity in entities: + services.append(entity.to_model()) + return [service for service in services] + + def get_service_by_name(self, name: str, subject: User) -> Service: + """Service method getting services by id.""" + query = select(ServiceEntity).where( + and_( + ServiceEntity.name == name, ServiceEntity.program.in_(subject.program) + ) + ) + entity = self._session.scalars(query).one_or_none() + + if entity is None: + raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned") + + return entity.to_model() + + def create(self, subject: User, service: Service) -> Service: + """Creates/adds a service to the table.""" + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot create service" + ) + + service_entity = ServiceEntity.from_model(service) + self._session.add(service_entity) + self._session.commit() + return service_entity.to_model() + + def update(self, subject: User, service: Service) -> Service: + """Updates a service if in the table.""" + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot update service" + ) + + query = select(ServiceEntity).where(ServiceEntity.id == service.id) + entity = self._session.scalars(query).one_or_none() + + if entity is None: + raise ServiceNotFoundException( + "The service you are searching for does not exist." + ) + + entity.name = service.name + entity.status = service.status + entity.summary = service.summary + entity.requirements = service.requirements + entity.program = service.program + self._session.commit() + + return entity.to_model() + + def delete(self, subject: User, service: Service) -> None: + """Deletes a service from the table.""" + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") + + query = select(ServiceEntity).where(ServiceEntity.id == service.id) + entity = self._session.scalars(query).one_or_none() + + if entity is None: + raise ServiceNotFoundException( + "The service you are searching for does not exist." + ) + + self._session.delete(entity) + self._session.commit() diff --git a/backend/services/tag.py b/backend/services/tag.py index dfc369a..b2299bd 100644 --- a/backend/services/tag.py +++ b/backend/services/tag.py @@ -1,20 +1,52 @@ -from fastapi import Depends -from ..database import db_session -from sqlalchemy.orm import Session -from ..models.tag_model import Tag -from ..entities.tag_entity import TagEntity -from sqlalchemy import select - - -class TagService: - - def __init__(self, session: Session = Depends(db_session)): - self._session = session - - def all(self) -> list[Tag]: - """Returns a list of all Tags""" - - query = select(TagEntity) - entities = self._session.scalars(query).all() - - return [entity.to_model() for entity in entities] +from fastapi import Depends + +from backend.models.enum_for_models import UserTypeEnum +from backend.models.user_model import User +from backend.services.exceptions import TagNotFoundException +from ..database import db_session +from sqlalchemy.orm import Session +from ..models.tag_model import Tag +from ..entities.tag_entity import TagEntity +from sqlalchemy import select + +# Add in checks for user permission? +class TagService: + + def __init__(self, session: Session = Depends(db_session)): + self._session = session + + def get_all(self) -> list[Tag]: + """Returns a list of all Tags""" + query = select(TagEntity) + entities = self._session.scalars(query).all() + + return [entity.to_model() for entity in entities] + + def create(self, subject: User, tag: Tag) -> Tag: + entity = TagEntity.from_model(tag) + self._session.add(entity) + self._session.commit() + return entity.to_model() + + def update(self, subject: User, tag: Tag) -> Tag: + query = select(TagEntity).where(TagEntity.id == tag.id) + entity = self._session.scalars(query).one_or_none() + + if entity is None: + raise TagNotFoundException(f"Tag with id {tag.id} does not exist") + + entity.content = tag.content + self._session.commit() + return entity.to_model() + + + def delete(self, subject: User, tag: Tag) -> None: + query = select(TagEntity).where(TagEntity.id == tag.id) + entity = self._session.scalars(query).one_or_none() + + if entity is None: + raise TagNotFoundException(f"Tag with id {tag.id} does not exist") + + self._session.delete(entity) + self._session.commit() + diff --git a/compass/app/resource/page.tsx b/compass/app/resource/page.tsx index 68a620f..7cbd2c1 100644 --- a/compass/app/resource/page.tsx +++ b/compass/app/resource/page.tsx @@ -27,7 +27,7 @@ export default function Page() { ); const resourcesAPI: Resource[] = await userListData.json(); - + setResources(resourcesAPI); } diff --git a/compass/components/Drawer/Drawer.tsx b/compass/components/Drawer/Drawer.tsx index 17b4557..da6fd8c 100644 --- a/compass/components/Drawer/Drawer.tsx +++ b/compass/components/Drawer/Drawer.tsx @@ -1,257 +1,255 @@ -import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; -import React, { useState } from "react"; -import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; -import { - StarIcon as SolidStarIcon, - EnvelopeIcon, - UserIcon, -} from "@heroicons/react/24/solid"; -import { - ArrowsPointingOutIcon, - ArrowsPointingInIcon, - StarIcon as OutlineStarIcon, - ListBulletIcon, -} from "@heroicons/react/24/outline"; -import TagsInput from "../TagsInput/Index"; - -type DrawerProps = { - title: string; - children: ReactNode; - onClick?: (event: React.MouseEvent) => void; - type?: "button" | "submit" | "reset"; // specify possible values for type - disabled?: boolean; - editableContent?: any; - onSave?: (content: any) => void; - rowContent?: any; - setData: Dispatch>; -}; - -interface EditContent { - content: string; - isEditing: boolean; -} - -const Drawer: FunctionComponent = ({ - title, - children, - onSave, - editableContent, - rowContent, - setData, -}) => { - const [isOpen, setIsOpen] = useState(false); - const [isFull, setIsFull] = useState(false); - const [isFavorite, setIsFavorite] = useState(false); - const [tempRowContent, setTempRowContent] = useState(rowContent); - - const onRowUpdate = (updatedRow: any) => { - setData((prevData: any) => ( - prevData.map((row: any) => ( - row.id === updatedRow.id - ? updatedRow - : row - )) - )) - }; - - const handleTempRowContentChange = (e) => { - const { name, value } = e.target; - console.log(name); - console.log(value); - setTempRowContent((prevContent) => ({ - ...prevContent, - [name]: value, - })); - }; - - const handleEnterPress = (e) => { - if (e.key === "Enter") { - e.preventDefault(); - // Update the rowContent with the temporaryRowContent - if (onRowUpdate) { - onRowUpdate(tempRowContent); - } - } - }; - - const toggleDrawer = () => { - setIsOpen(!isOpen); - if (isFull) { - setIsFull(!isFull); - } - }; - - const toggleDrawerFullScreen = () => setIsFull(!isFull); - - const toggleFavorite = () => setIsFavorite(!isFavorite); - - const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${ - isOpen ? "translate-x-0 shadow-xl" : "translate-x-full" - } ${isFull ? "w-full" : "w-1/2"}`; - - const iconComponent = isFull ? ( - - ) : ( - - ); - - const favoriteIcon = isFavorite ? ( - - ) : ( - - ); - - const [presetOptions, setPresetOptions] = useState([ - "administrator", - "volunteer", - "employee", - ]); - const [rolePresetOptions, setRolePresetOptions] = useState([ - "domestic", - "community", - "economic", - ]); - const [tagColors, setTagColors] = useState(new Map()); - - const getTagColor = (tag: string) => { - if (!tagColors.has(tag)) { - const colors = [ - "bg-cyan-100", - "bg-blue-100", - "bg-green-100", - "bg-yellow-100", - "bg-purple-100", - ]; - const randomColor = - colors[Math.floor(Math.random() * colors.length)]; - setTagColors(new Map(tagColors).set(tag, randomColor)); - } - return tagColors.get(tag); - }; - - return ( -
- -
-
-
-
- - - -

- {rowContent.username} -

-
-
- - - -
-
-
- - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
-
- - - - - -
- - Username - -
- - Role - -
- - Email - -
- - Type of Program - {/* {rowContent.program} */} - -
-
-
-
-
- ); -}; - -export default Drawer; +import { Dispatch, FunctionComponent, ReactNode, SetStateAction } from "react"; +import React, { useState } from "react"; +import { ChevronDoubleLeftIcon } from "@heroicons/react/24/solid"; +import { + StarIcon as SolidStarIcon, + EnvelopeIcon, + UserIcon, +} from "@heroicons/react/24/solid"; +import { + ArrowsPointingOutIcon, + ArrowsPointingInIcon, + StarIcon as OutlineStarIcon, + ListBulletIcon, +} from "@heroicons/react/24/outline"; +import TagsInput from "../TagsInput/Index"; + +type DrawerProps = { + title: string; + children: ReactNode; + onClick?: (event: React.MouseEvent) => void; + type?: "button" | "submit" | "reset"; // specify possible values for type + disabled?: boolean; + editableContent?: any; + onSave?: (content: any) => void; + rowContent?: any; + setData: Dispatch>; +}; + +interface EditContent { + content: string; + isEditing: boolean; +} + +const Drawer: FunctionComponent = ({ + title, + children, + onSave, + editableContent, + rowContent, + setData, +}) => { + const [isOpen, setIsOpen] = useState(false); + const [isFull, setIsFull] = useState(false); + const [isFavorite, setIsFavorite] = useState(false); + const [tempRowContent, setTempRowContent] = useState(rowContent); + + const onRowUpdate = (updatedRow: any) => { + setData((prevData: any) => + prevData.map((row: any) => + row.id === updatedRow.id ? updatedRow : row + ) + ); + }; + + const handleTempRowContentChange = (e) => { + const { name, value } = e.target; + console.log(name); + console.log(value); + setTempRowContent((prevContent) => ({ + ...prevContent, + [name]: value, + })); + }; + + const handleEnterPress = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + // Update the rowContent with the temporaryRowContent + if (onRowUpdate) { + onRowUpdate(tempRowContent); + } + } + }; + + const toggleDrawer = () => { + setIsOpen(!isOpen); + if (isFull) { + setIsFull(!isFull); + } + }; + + const toggleDrawerFullScreen = () => setIsFull(!isFull); + + const toggleFavorite = () => setIsFavorite(!isFavorite); + + const drawerClassName = `fixed top-0 right-0 w-1/2 h-full bg-white transform ease-in-out duration-300 z-20 ${ + isOpen ? "translate-x-0 shadow-xl" : "translate-x-full" + } ${isFull ? "w-full" : "w-1/2"}`; + + const iconComponent = isFull ? ( + + ) : ( + + ); + + const favoriteIcon = isFavorite ? ( + + ) : ( + + ); + + const [presetOptions, setPresetOptions] = useState([ + "administrator", + "volunteer", + "employee", + ]); + const [rolePresetOptions, setRolePresetOptions] = useState([ + "domestic", + "community", + "economic", + ]); + const [tagColors, setTagColors] = useState(new Map()); + + const getTagColor = (tag: string) => { + if (!tagColors.has(tag)) { + const colors = [ + "bg-cyan-100", + "bg-blue-100", + "bg-green-100", + "bg-yellow-100", + "bg-purple-100", + ]; + const randomColor = + colors[Math.floor(Math.random() * colors.length)]; + setTagColors(new Map(tagColors).set(tag, randomColor)); + } + return tagColors.get(tag); + }; + + return ( +
+ +
+
+
+
+ + + +

+ {rowContent.username} +

+
+
+ + + +
+
+
+ + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+
+ + + + + +
+ + Username + +
+ + Role + +
+ + Email + +
+ + Type of Program + {/* {rowContent.program} */} + +
+
+
+
+
+ ); +}; + +export default Drawer; diff --git a/compass/components/Table/ResourceTable.tsx b/compass/components/Table/ResourceTable.tsx index a02162a..6979498 100644 --- a/compass/components/Table/ResourceTable.tsx +++ b/compass/components/Table/ResourceTable.tsx @@ -8,24 +8,20 @@ import TagsInput from "@/components/TagsInput/Index"; import Resource from "@/utils/models/Resource"; type ResourceTableProps = { - data: Resource[], - setData: Dispatch> -} + data: Resource[]; + setData: Dispatch>; +}; /** * Table componenet used for displaying resources * @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 ) { - const columnHelper = createColumnHelper(); +export default function ResourceTable({ data, setData }: ResourceTableProps) { + const columnHelper = createColumnHelper(); // Set up tag handling - const programProps = useTagsHandler([ - "community", - "domestic", - "economic", - ]) + const programProps = useTagsHandler(["community", "domestic", "economic"]); // Define Tanstack columns const columns: ColumnDef[] = [ @@ -66,10 +62,7 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) { ), cell: (info) => ( - + ), }), @@ -85,5 +78,5 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) { }), ]; - return + return
; } diff --git a/compass/components/Table/RowOpenAction.tsx b/compass/components/Table/RowOpenAction.tsx index 3678630..a6aeb52 100644 --- a/compass/components/Table/RowOpenAction.tsx +++ b/compass/components/Table/RowOpenAction.tsx @@ -1,34 +1,38 @@ -import Drawer from "@/components/Drawer/Drawer"; -import DataPoint from "@/utils/models/DataPoint"; -import { Dispatch, SetStateAction, useState } from "react"; - -type RowOpenActionProps = { - title: string, - rowData: T, - setData: Dispatch> -} - -export function RowOpenAction({ title, rowData, setData }: RowOpenActionProps) { - const [pageContent, setPageContent] = useState(""); - - const handleDrawerContentChange = (newContent: string) => { - setPageContent(newContent); - }; - - return ( -
- {title} - - - {pageContent} - - -
- ); -}; +import Drawer from "@/components/Drawer/Drawer"; +import DataPoint from "@/utils/models/DataPoint"; +import { Dispatch, SetStateAction, useState } from "react"; + +type RowOpenActionProps = { + title: string; + rowData: T; + setData: Dispatch>; +}; + +export function RowOpenAction({ + title, + rowData, + setData, +}: RowOpenActionProps) { + const [pageContent, setPageContent] = useState(""); + + const handleDrawerContentChange = (newContent: string) => { + setPageContent(newContent); + }; + + return ( +
+ {title} + + + {pageContent} + + +
+ ); +} diff --git a/compass/components/Table/ServiceTable.tsx b/compass/components/Table/ServiceTable.tsx index 05e42b2..794452e 100644 --- a/compass/components/Table/ServiceTable.tsx +++ b/compass/components/Table/ServiceTable.tsx @@ -1,108 +1,103 @@ -import { Bars2Icon } from "@heroicons/react/24/solid"; -import { Dispatch, SetStateAction } from "react"; -import useTagsHandler from "@/components/TagsInput/TagsHandler"; -import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; -import Table from "@/components/Table/Table"; -import { RowOpenAction } from "@/components/Table/RowOpenAction"; -import TagsInput from "@/components/TagsInput/Index"; -import Service from "@/utils/models/Service"; - -type ServiceTableProps = { - data: Service[], - setData: Dispatch> -} - -/** - * Table componenet used for displaying services - * @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 ) { - const columnHelper = createColumnHelper(); - - // Set up tag handling - const programProps = useTagsHandler([ - "community", - "domestic", - "economic", - ]) - - // TODO: Dynamically or statically get full list of preset requirement tag options - const requirementProps = useTagsHandler([ - 'anonymous', - 'confidential', - 'referral required', - 'safety assessment', - 'intake required', - 'income eligibility', - 'initial assessment', - ]) - - // Define Tanstack columns - const columns: ColumnDef[] = [ - columnHelper.accessor("name", { - header: () => ( - <> - Name - - ), - cell: (info) => ( - - ), - }), - columnHelper.accessor("status", { - header: () => ( - <> - Status - - ), - cell: (info) => ( - {info.getValue()} - ), - }), - columnHelper.accessor("program", { - header: () => ( - <> - Program - - ), - cell: (info) => ( - - ), - }), - columnHelper.accessor("requirements", { - header: () => ( - <> - Requirements - - ), - cell: (info) => ( - // TODO: Setup different tag handler for requirements - - ), - }), - - columnHelper.accessor("summary", { - header: () => ( - <> - Summary - - ), - cell: (info) => ( - {info.getValue()} - ), - }), - ]; - - return
-}; +import { Bars2Icon } from "@heroicons/react/24/solid"; +import { Dispatch, SetStateAction } from "react"; +import useTagsHandler from "@/components/TagsInput/TagsHandler"; +import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; +import Table from "@/components/Table/Table"; +import { RowOpenAction } from "@/components/Table/RowOpenAction"; +import TagsInput from "@/components/TagsInput/Index"; +import Service from "@/utils/models/Service"; + +type ServiceTableProps = { + data: Service[]; + setData: Dispatch>; +}; + +/** + * Table componenet used for displaying services + * @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) { + const columnHelper = createColumnHelper(); + + // Set up tag handling + const programProps = useTagsHandler(["community", "domestic", "economic"]); + + // TODO: Dynamically or statically get full list of preset requirement tag options + const requirementProps = useTagsHandler([ + "anonymous", + "confidential", + "referral required", + "safety assessment", + "intake required", + "income eligibility", + "initial assessment", + ]); + + // Define Tanstack columns + const columns: ColumnDef[] = [ + columnHelper.accessor("name", { + header: () => ( + <> + Name + + ), + cell: (info) => ( + + ), + }), + columnHelper.accessor("status", { + header: () => ( + <> + Status + + ), + cell: (info) => ( + {info.getValue()} + ), + }), + columnHelper.accessor("program", { + header: () => ( + <> + Program + + ), + cell: (info) => ( + + ), + }), + columnHelper.accessor("requirements", { + header: () => ( + <> + Requirements + + ), + cell: (info) => ( + // TODO: Setup different tag handler for requirements + + ), + }), + + columnHelper.accessor("summary", { + header: () => ( + <> + Summary + + ), + cell: (info) => ( + {info.getValue()} + ), + }), + ]; + + return
; +} diff --git a/compass/components/Table/Table.tsx b/compass/components/Table/Table.tsx index ef0a002..b0a3bd1 100644 --- a/compass/components/Table/Table.tsx +++ b/compass/components/Table/Table.tsx @@ -1,224 +1,225 @@ -import { - Row, - ColumnDef, - useReactTable, - getCoreRowModel, - flexRender, - createColumnHelper - } from "@tanstack/react-table"; -import { - ChangeEvent, - useState, - useEffect, - Key, - Dispatch, - SetStateAction -} from "react"; -import { TableAction } from "./TableAction"; -import { PlusIcon } from "@heroicons/react/24/solid"; -import { rankItem } from "@tanstack/match-sorter-utils"; -import { RowOptionMenu } from "./RowOptionMenu"; -import DataPoint from "@/utils/models/DataPoint"; - -type TableProps = { - data: T[], - setData: Dispatch>, - columns: ColumnDef[] -}; - -/** Fuzzy search function */ -const fuzzyFilter = ( - row: Row, - columnId: string, - value: any, - addMeta: (meta: any) => void -) => { - // Rank the item - const itemRank = rankItem(row.getValue(columnId), value); - - // Store the ranking info - addMeta(itemRank); - - // Return if the item should be filtered in/out - return itemRank.passed; -}; - -/** - * General componenet that holds shared functionality for any data table component - * @param props.data Stateful list of data to be held in the table - * @param props.setData State setter for the list of data - * @param props.columns Column definitions made with Tanstack columnHelper -*/ -export default function Table({ data, setData, columns }: TableProps) { - const columnHelper = createColumnHelper(); - - /** Sorting function based on visibility */ - const visibilitySort = (a: T, b: T) => ( - a.visible === b.visible - ? 0 - : a.visible ? -1 : 1 - ) - - // Sort data on load - useEffect(() => { - setData(prevData => prevData.sort(visibilitySort)) - }, [setData]); - - // Data manipulation methods - // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData) - const deleteData = (dataId: number) => { - console.log(data); - setData((currentData) => - currentData.filter((data) => data.id !== dataId) - ); - }; - - const hideData = (dataId: number) => { - console.log(`Toggling visibility for data with ID: ${dataId}`); - setData(currentData => { - const newData = currentData - .map(data => ( - data.id === dataId - ? { ...data, visible: !data.visible } - : data - )) - .sort(visibilitySort); - - console.log(newData); - return newData; - }); - }; - - const addData = () => { - setData([...data]); - }; - - // Add data manipulation options to the first column - columns.unshift( - columnHelper.display({ - id: "options", - cell: (props) => ( - deleteData(props.row.original.id)} - onHide={() => hideData(props.row.original.id)} - /> - ), - }) - ) - - // Searching - const [query, setQuery] = useState(""); - const handleSearchChange = (e: ChangeEvent) => { - const target = e.target as HTMLInputElement; - setQuery(String(target.value)); - }; - - const handleCellChange = (e: ChangeEvent, key: Key) => { - const target = e.target as HTMLInputElement; - console.log(key); - }; - - // TODO: Filtering - - // TODO: Sorting - - // Define Tanstack table - const table = useReactTable({ - columns, - data, - filterFns: { - fuzzy: fuzzyFilter, - }, - state: { - globalFilter: query, - }, - onGlobalFilterChange: setQuery, - globalFilterFn: fuzzyFilter, - getCoreRowModel: getCoreRowModel(), - }); - - const handleRowData = (row: any) => { - const rowData: any = {}; - row.cells.forEach((cell: any) => { - rowData[cell.column.id] = cell.value; - }); - // Use rowData object containing data from all columns for the current row - console.log(rowData); - return rowData; - }; - - return ( -
-
- -
-
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header, i) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => { - // Individual row - const isDataVisible = row.original.visible; - const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ - !isDataVisible ? "bg-gray-200 text-gray-500" : "" - }`; - return ( - - {row.getVisibleCells().map((cell, i) => ( - - ))} - - ); - })} - - - - - - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {flexRender( - cell.column.columnDef.cell, - cell.getContext() - )} -
- - - New - -
- - ); -}; +import { + Row, + ColumnDef, + useReactTable, + getCoreRowModel, + flexRender, + createColumnHelper, +} from "@tanstack/react-table"; +import { + ChangeEvent, + useState, + useEffect, + Key, + Dispatch, + SetStateAction, +} from "react"; +import { TableAction } from "./TableAction"; +import { PlusIcon } from "@heroicons/react/24/solid"; +import { rankItem } from "@tanstack/match-sorter-utils"; +import { RowOptionMenu } from "./RowOptionMenu"; +import DataPoint from "@/utils/models/DataPoint"; + +type TableProps = { + data: T[]; + setData: Dispatch>; + columns: ColumnDef[]; +}; + +/** Fuzzy search function */ +const fuzzyFilter = ( + row: Row, + columnId: string, + value: any, + addMeta: (meta: any) => void +) => { + // Rank the item + const itemRank = rankItem(row.getValue(columnId), value); + + // Store the ranking info + addMeta(itemRank); + + // Return if the item should be filtered in/out + return itemRank.passed; +}; + +/** + * General componenet that holds shared functionality for any data table component + * @param props.data Stateful list of data to be held in the table + * @param props.setData State setter for the list of data + * @param props.columns Column definitions made with Tanstack columnHelper + */ +export default function Table({ + data, + setData, + columns, +}: TableProps) { + const columnHelper = createColumnHelper(); + + /** Sorting function based on visibility */ + const visibilitySort = (a: T, b: T) => + a.visible === b.visible ? 0 : a.visible ? -1 : 1; + + // Sort data on load + useEffect(() => { + setData((prevData) => prevData.sort(visibilitySort)); + }, [setData]); + + // Data manipulation methods + // TODO: Connect data manipulation methods to the database (deleteData, hideData, addData) + const deleteData = (dataId: number) => { + console.log(data); + setData((currentData) => + currentData.filter((data) => data.id !== dataId) + ); + }; + + const hideData = (dataId: number) => { + console.log(`Toggling visibility for data with ID: ${dataId}`); + setData((currentData) => { + const newData = currentData + .map((data) => + data.id === dataId + ? { ...data, visible: !data.visible } + : data + ) + .sort(visibilitySort); + + console.log(newData); + return newData; + }); + }; + + const addData = () => { + setData([...data]); + }; + + // Add data manipulation options to the first column + columns.unshift( + columnHelper.display({ + id: "options", + cell: (props) => ( + deleteData(props.row.original.id)} + onHide={() => hideData(props.row.original.id)} + /> + ), + }) + ); + + // Searching + const [query, setQuery] = useState(""); + const handleSearchChange = (e: ChangeEvent) => { + const target = e.target as HTMLInputElement; + setQuery(String(target.value)); + }; + + const handleCellChange = (e: ChangeEvent, key: Key) => { + const target = e.target as HTMLInputElement; + console.log(key); + }; + + // TODO: Filtering + + // TODO: Sorting + + // Define Tanstack table + const table = useReactTable({ + columns, + data, + filterFns: { + fuzzy: fuzzyFilter, + }, + state: { + globalFilter: query, + }, + onGlobalFilterChange: setQuery, + globalFilterFn: fuzzyFilter, + getCoreRowModel: getCoreRowModel(), + }); + + const handleRowData = (row: any) => { + const rowData: any = {}; + row.cells.forEach((cell: any) => { + rowData[cell.column.id] = cell.value; + }); + // Use rowData object containing data from all columns for the current row + console.log(rowData); + return rowData; + }; + + return ( +
+
+ +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header, i) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => { + // Individual row + const isDataVisible = row.original.visible; + const rowClassNames = `text-gray-800 border-y lowercase hover:bg-gray-50 ${ + !isDataVisible ? "bg-gray-200 text-gray-500" : "" + }`; + return ( + + {row.getVisibleCells().map((cell, i) => ( + + ))} + + ); + })} + + + + + + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} +
+ + + New + +
+
+ ); +} diff --git a/compass/components/Table/UserTable.tsx b/compass/components/Table/UserTable.tsx index 0f81d66..a480dc8 100644 --- a/compass/components/Table/UserTable.tsx +++ b/compass/components/Table/UserTable.tsx @@ -1,4 +1,8 @@ -import { ArrowDownCircleIcon, AtSymbolIcon, Bars2Icon } from "@heroicons/react/24/solid"; +import { + ArrowDownCircleIcon, + AtSymbolIcon, + Bars2Icon, +} from "@heroicons/react/24/solid"; import { Dispatch, SetStateAction } from "react"; import useTagsHandler from "@/components/TagsInput/TagsHandler"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; @@ -8,31 +12,27 @@ import TagsInput from "@/components/TagsInput/Index"; import User from "@/utils/models/User"; type UserTableProps = { - data: User[], - setData: Dispatch> -} + data: User[]; + setData: Dispatch>; +}; /** * Table componenet used for displaying users * @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 }: UserTableProps) { const columnHelper = createColumnHelper(); - + // Set up tag handling const roleProps = useTagsHandler([ "administrator", "volunteer", "employee", - ]) - - const programProps = useTagsHandler([ - "community", - "domestic", - "economic", - ]) - + ]); + + const programProps = useTagsHandler(["community", "domestic", "economic"]); + // Define Tanstack columns const columns: ColumnDef[] = [ columnHelper.accessor("username", { @@ -57,10 +57,7 @@ export default function UserTable({ data, setData }: UserTableProps ) { ), cell: (info) => ( - + ), }), columnHelper.accessor("email", { @@ -74,7 +71,7 @@ export default function UserTable({ data, setData }: UserTableProps ) { {info.getValue()} ), - }), + }), columnHelper.accessor("program", { header: () => ( <> @@ -83,13 +80,10 @@ export default function UserTable({ data, setData }: UserTableProps ) { ), cell: (info) => ( - + ), }), ]; - return data={data} setData={setData} columns={columns}/> + return data={data} setData={setData} columns={columns} />; } diff --git a/compass/components/TagsInput/TagsHandler.tsx b/compass/components/TagsInput/TagsHandler.tsx index e8fde75..179e367 100644 --- a/compass/components/TagsInput/TagsHandler.tsx +++ b/compass/components/TagsInput/TagsHandler.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useState } from "react"; /** * Custom hook used to handle the state of tag options and colors @@ -31,5 +31,5 @@ export default function useTagsHandler(initialOptions: string[]) { return tagColors.get(tag) as string; }; - return { presetOptions, setPresetOptions, getTagColor } -} \ No newline at end of file + return { presetOptions, setPresetOptions, getTagColor }; +} diff --git a/compass/utils/models/DataPoint.ts b/compass/utils/models/DataPoint.ts index b7dcacb..4a6a321 100644 --- a/compass/utils/models/DataPoint.ts +++ b/compass/utils/models/DataPoint.ts @@ -6,4 +6,4 @@ interface DataPoint { visible: boolean; } -export default DataPoint; \ No newline at end of file +export default DataPoint;