API Routes for Resources and Services (#40)

* Implemented API routes for getting all, creating, updating, and deleting resources, services, and tags.

* Updated main.py for API routes to include tags and rolled entities back.

* Created API routes for create, update, delete, get_all, and get_by_name. Deleted service methods for get by id.

* Defaults created_at to current time

* Write markdown file for HTTP requests using curl

---------

Co-authored-by: pmoharana-cmd <pmoharana032474@gmail.com>
This commit is contained in:
Aidan Kim 2024-11-05 19:12:03 -05:00 committed by GitHub
parent 99e43c7b30
commit 7d705ac743
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 1455 additions and 1226 deletions

View File

@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from backend.models.user_model import User
from ..services import ResourceService, UserService from ..services import ResourceService, UserService
from ..models.resource_model import Resource from ..models.resource_model import Resource
@ -15,12 +17,40 @@ openapi_tags = {
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # 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"]) @api.get("", response_model=List[Resource], tags=["Resource"])
def get_all( def get_all(
user_id: str, uuid: str, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends()
resource_svc: ResourceService = Depends(),
user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(uuid)
return resource_svc.get_resource_by_user(subject) 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)

View File

@ -1,4 +1,6 @@
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from backend.models.user_model import User
from ..services import ServiceService, UserService from ..services import ServiceService, UserService
from ..models.service_model import Service from ..models.service_model import Service
@ -15,12 +17,38 @@ openapi_tags = {
# TODO: Add security using HTTP Bearer Tokens # TODO: Add security using HTTP Bearer Tokens
# TODO: Enable authorization by passing user uuid to API # TODO: Enable authorization by passing user uuid to API
# TODO: Create custom exceptions # 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"]) @api.get("", response_model=List[Service], tags=["Service"])
def get_all( def get_all(
user_id: str, uuid: str, user_svc: UserService = Depends(), service_svc: ServiceService = Depends()
service_svc: ServiceService = Depends(),
user_svc: UserService = Depends(),
): ):
subject = user_svc.get_user_by_uuid(user_id) subject = user_svc.get_user_by_uuid(uuid)
return service_svc.get_service_by_user(subject) 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)

51
backend/api/tag.py Normal file
View File

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

147
backend/api/test_routes.md Normal file
View File

@ -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"
}'
```

View File

@ -23,3 +23,4 @@ class ServiceTagEntity(EntityBase):
# relationships # relationships
service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags")
tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags")

View File

@ -45,6 +45,7 @@ class TagEntity(EntityBase):
return cls( return cls(
id=model.id, id=model.id,
created_at=model.created_at,
content=model.id, content=model.id,
) )
@ -58,8 +59,6 @@ class TagEntity(EntityBase):
return Tag( return Tag(
id=self.id, id=self.id,
create_at=self.created_at,
content=self.content, content=self.content,
) )

View File

@ -2,7 +2,8 @@ from fastapi import FastAPI, Request
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.middleware.gzip import GZipMiddleware from fastapi.middleware.gzip import GZipMiddleware
from .api import user, health, service, resource
from .api import user, health, service, resource, tag
description = """ description = """
Welcome to the **COMPASS** RESTful Application Programming Interface. Welcome to the **COMPASS** RESTful Application Programming Interface.
@ -17,12 +18,13 @@ app = FastAPI(
health.openapi_tags, health.openapi_tags,
service.openapi_tags, service.openapi_tags,
resource.openapi_tags, resource.openapi_tags,
tag.openapi_tags
], ],
) )
app.add_middleware(GZipMiddleware) app.add_middleware(GZipMiddleware)
feature_apis = [user, health, service, resource] feature_apis = [user, health, service, resource, tag]
for feature_api in feature_apis: for feature_api in feature_apis:
app.include_router(feature_api.api) app.include_router(feature_api.api)

View File

@ -12,4 +12,4 @@ class Resource(BaseModel):
summary: str = Field(..., max_length=300, description="The summary of the resource") summary: str = Field(..., max_length=300, description="The summary of the resource")
link: str = Field(..., max_length=150, description="link to the resource") link: str = Field(..., max_length=150, description="link to the resource")
program: ProgramTypeEnum program: ProgramTypeEnum
created_at: Optional[datetime] created_at: Optional[datetime] = datetime.now()

View File

@ -8,7 +8,7 @@ from .enum_for_models import ProgramTypeEnum
class Service(BaseModel): class Service(BaseModel):
id: int | None = None id: int | None = None
created_at: datetime | None = None created_at: datetime | None = datetime.now()
name: str name: str
status: str status: str
summary: str summary: str

View File

@ -10,4 +10,4 @@ class Tag(BaseModel):
content: str = Field( content: str = Field(
..., max_length=600, description="content associated with the tag" ..., max_length=600, description="content associated with the tag"
) )
created_at: datetime | None = None created_at: datetime | None = datetime.now()

View File

@ -14,5 +14,5 @@ class User(BaseModel):
group: str group: str
program: List[ProgramTypeEnum] program: List[ProgramTypeEnum]
role: UserTypeEnum role: UserTypeEnum
created_at: Optional[datetime] created_at: Optional[datetime] = datetime.now()
uuid: str | None = None uuid: str | None = None

View File

@ -20,6 +20,8 @@ class UserPermissionException(Exception):
class ServiceNotFoundException(Exception): class ServiceNotFoundException(Exception):
"""Exception for when the service being requested is not in the table.""" """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): class ProgramNotAssignedException(Exception):
"""Exception for when the user does not have correct access for requested services.""" """Exception for when the user does not have correct access for requested services."""

View File

@ -1,12 +1,12 @@
from fastapi import Depends from fastapi import Depends
from ..database import db_session from ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy import select from sqlalchemy import and_, select
from ..models.resource_model import Resource from ..models.resource_model import Resource
from ..entities.resource_entity import ResourceEntity from ..entities.resource_entity import ResourceEntity
from ..models.user_model import User, UserTypeEnum from ..models.user_model import User, UserTypeEnum
from .exceptions import ResourceNotFoundException from .exceptions import ProgramNotAssignedException, ResourceNotFoundException
class ResourceService: class ResourceService:
@ -14,25 +14,40 @@ class ResourceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = 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""" """Resource method getting all of the resources that a user has access to based on role"""
if subject.role != UserTypeEnum.VOLUNTEER: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ResourceEntity) query = select(ResourceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [resource.to_model() for resource in entities] return [resource.to_model() for resource in entities]
else: else:
programs = subject.program programs = subject.program
resources = [] resources = []
for program in programs: for program in programs:
query = select(ResourceEntity).filter(ResourceEntity.program == program) entities = (
entities = self._session.scalars(query).all() self._session.query(ResourceEntity)
.where(ResourceEntity.program == program)
.all()
)
for entity in entities: 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. Creates a resource based on the input object and adds it to the table if the user has the right permissions.
Parameters: Parameters:
@ -41,43 +56,16 @@ class ResourceService:
Returns: Returns:
Resource: Object added to table Resource: Object added to table
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to add resources in this program." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
resource_entity = ResourceEntity.from_model(resource) resource_entity = ResourceEntity.from_model(resource)
self._session.add(resource_entity) self._session.add(resource_entity)
self._session.commit() self._session.commit()
return resource_entity.to_model() return resource_entity.to_model()
def get_by_id(self, user: User, id: int) -> Resource: def update(self, subject: User, resource: Resource) -> 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:
""" """
Update the resource if the user has access Update the resource if the user has access
Parameters: Parameters:
@ -88,28 +76,24 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding ID ResourceNotFoundException: If no resource is found with the corresponding ID
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to update this resource." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None entity = self._session.scalars(query).one_or_none()
if entity is None:
if obj is None:
raise ResourceNotFoundException( raise ResourceNotFoundException(
f"No resource found with matching id: {resource.id}" f"No resource found with matching id: {resource.id}"
) )
entity.name = resource.name
obj.name = resource.name entity.summary = resource.summary
obj.summary = resource.summary entity.link = resource.link
obj.link = resource.link entity.program = resource.program
obj.program = resource.program
self._session.commit() self._session.commit()
return entity.to_model()
return obj.to_model() def delete(self, subject: User, resource: Resource) -> None:
def delete(self, user: User, id: int) -> None:
""" """
Delete resource based on id that the user has access to Delete resource based on id that the user has access to
Parameters: Parameters:
@ -118,23 +102,17 @@ class ResourceService:
Raises: Raises:
ResourceNotFoundException: If no resource is found with the corresponding id ResourceNotFoundException: If no resource is found with the corresponding id
""" """
if user.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise PermissionError( raise ProgramNotAssignedException(
"User does not have permission to delete this resource." f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
query = select(ResourceEntity).where(ResourceEntity.id == resource.id)
resource = ( entity = self._session.scalars(query).one_or_none()
self._session.query(ResourceEntity) if entity is None:
.filter( raise ResourceNotFoundException(
ResourceEntity.id == id, f"No resource found with matching id: {resource.id}"
) )
.one_or_none() self._session.delete(entity)
)
if resource is None:
raise ResourceNotFoundException(f"No resource found with matching id: {id}")
self._session.delete(resource)
self._session.commit() self._session.commit()
def get_by_slug(self, user: User, search_string: str) -> list[Resource]: def get_by_slug(self, user: User, search_string: str) -> list[Resource]:
@ -150,7 +128,7 @@ class ResourceService:
""" """
query = select(ResourceEntity).where( query = select(ResourceEntity).where(
ResourceEntity.name.ilike(f"%{search_string}%"), ResourceEntity.name.ilike(f"%{search_string}%"),
ResourceEntity.program.in_(user.program) ResourceEntity.program.in_(user.program),
) )
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()

View File

@ -19,62 +19,34 @@ class ServiceService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def get_service_by_program(self, program: ProgramTypeEnum) -> list[Service]: def get_service_by_user(self, subject: User) -> list[Service]:
"""Service method getting services belonging to a particular program.""" """Resource method getting all of the resources that a user has access to based on role"""
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: if subject.role != UserTypeEnum.VOLUNTEER:
query = select(ServiceEntity) query = select(ServiceEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [service.to_model() for service in entities] return [service.to_model() for service in entities]
else: else:
programs = subject.program programs = subject.program
services = [] services = []
for program in programs: for program in programs:
query = select(ServiceEntity).filter(ServiceEntity.program == program) entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all()
entities = self._session.scalars(query).all()
for entity in entities: for entity in entities:
services.append(entity) services.append(entity.to_model())
return [service for service in services]
return [service.to_model() for service in services] def get_service_by_name(self, name: str, subject: User) -> Service:
"""Service method getting services by id."""
def get_all(self, subject: User) -> list[Service]: query = select(ServiceEntity).where(
"""Service method retrieving all of the services in the table.""" and_(
if subject.role == UserTypeEnum.VOLUNTEER: ServiceEntity.name == name, ServiceEntity.program.in_(subject.program)
raise ProgramNotAssignedException(
f"User is not {UserTypeEnum.ADMIN} or {UserTypeEnum.VOLUNTEER}, cannot get all"
) )
)
entity = self._session.scalars(query).one_or_none()
query = select(ServiceEntity) if entity is None:
entities = self._session.scalars(query).all() raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned")
return [service.to_model() for service in entities] return entity.to_model()
def create(self, subject: User, service: Service) -> Service: def create(self, subject: User, service: Service) -> Service:
"""Creates/adds a service to the table.""" """Creates/adds a service to the table."""
@ -95,33 +67,35 @@ class ServiceService:
f"User is not {UserTypeEnum.ADMIN}, cannot update service" f"User is not {UserTypeEnum.ADMIN}, cannot update service"
) )
service_entity = self._session.get(ServiceEntity, service.id) query = select(ServiceEntity).where(ServiceEntity.id == service.id)
entity = self._session.scalars(query).one_or_none()
if service_entity is None: if entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
service_entity.name = service.name entity.name = service.name
service_entity.status = service.status entity.status = service.status
service_entity.summary = service.summary entity.summary = service.summary
service_entity.requirements = service.requirements entity.requirements = service.requirements
service_entity.program = service.program entity.program = service.program
self._session.commit() self._session.commit()
return service_entity.to_model() return entity.to_model()
def delete(self, subject: User, service: Service) -> None: def delete(self, subject: User, service: Service) -> None:
"""Deletes a service from the table.""" """Deletes a service from the table."""
if subject.role != UserTypeEnum.ADMIN: if subject.role != UserTypeEnum.ADMIN:
raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}") raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}")
service_entity = self._session.get(ServiceEntity, service.id)
if service_entity is None: query = select(ServiceEntity).where(ServiceEntity.id == service.id)
entity = self._session.scalars(query).one_or_none()
if entity is None:
raise ServiceNotFoundException( raise ServiceNotFoundException(
"The service you are searching for does not exist." "The service you are searching for does not exist."
) )
self._session.delete(service_entity) self._session.delete(entity)
self._session.commit() self._session.commit()

View File

@ -1,20 +1,52 @@
from fastapi import Depends 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 ..database import db_session
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from ..models.tag_model import Tag from ..models.tag_model import Tag
from ..entities.tag_entity import TagEntity from ..entities.tag_entity import TagEntity
from sqlalchemy import select from sqlalchemy import select
# Add in checks for user permission?
class TagService: class TagService:
def __init__(self, session: Session = Depends(db_session)): def __init__(self, session: Session = Depends(db_session)):
self._session = session self._session = session
def all(self) -> list[Tag]: def get_all(self) -> list[Tag]:
"""Returns a list of all Tags""" """Returns a list of all Tags"""
query = select(TagEntity) query = select(TagEntity)
entities = self._session.scalars(query).all() entities = self._session.scalars(query).all()
return [entity.to_model() for entity in entities] 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()

View File

@ -45,13 +45,11 @@ const Drawer: FunctionComponent<DrawerProps> = ({
const [tempRowContent, setTempRowContent] = useState(rowContent); const [tempRowContent, setTempRowContent] = useState(rowContent);
const onRowUpdate = (updatedRow: any) => { const onRowUpdate = (updatedRow: any) => {
setData((prevData: any) => ( setData((prevData: any) =>
prevData.map((row: any) => ( prevData.map((row: any) =>
row.id === updatedRow.id row.id === updatedRow.id ? updatedRow : row
? updatedRow )
: row );
))
))
}; };
const handleTempRowContentChange = (e) => { const handleTempRowContentChange = (e) => {

View File

@ -8,24 +8,20 @@ import TagsInput from "@/components/TagsInput/Index";
import Resource from "@/utils/models/Resource"; import Resource from "@/utils/models/Resource";
type ResourceTableProps = { type ResourceTableProps = {
data: Resource[], data: Resource[];
setData: Dispatch<SetStateAction<Resource[]>> setData: Dispatch<SetStateAction<Resource[]>>;
} };
/** /**
* Table componenet used for displaying resources * Table componenet used for displaying resources
* @param props.data Stateful list of resources to be displayed by the table * @param props.data Stateful list of resources to be displayed by the table
* @param props.setData State setter for the list of resources * @param props.setData State setter for the list of resources
*/ */
export default function ResourceTable({ data, setData }: ResourceTableProps ) { export default function ResourceTable({ data, setData }: ResourceTableProps) {
const columnHelper = createColumnHelper<Resource>(); const columnHelper = createColumnHelper<Resource>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Resource, any>[] = [ const columns: ColumnDef<Resource, any>[] = [
@ -66,10 +62,7 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...programProps} />
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
@ -85,5 +78,5 @@ export default function ResourceTable({ data, setData }: ResourceTableProps ) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns}/> return <Table data={data} setData={setData} columns={columns} />;
} }

View File

@ -3,12 +3,16 @@ import DataPoint from "@/utils/models/DataPoint";
import { Dispatch, SetStateAction, useState } from "react"; import { Dispatch, SetStateAction, useState } from "react";
type RowOpenActionProps<T extends DataPoint> = { type RowOpenActionProps<T extends DataPoint> = {
title: string, title: string;
rowData: T, rowData: T;
setData: Dispatch<SetStateAction<T[]>> setData: Dispatch<SetStateAction<T[]>>;
} };
export function RowOpenAction<T extends DataPoint>({ title, rowData, setData }: RowOpenActionProps<T>) { export function RowOpenAction<T extends DataPoint>({
title,
rowData,
setData,
}: RowOpenActionProps<T>) {
const [pageContent, setPageContent] = useState(""); const [pageContent, setPageContent] = useState("");
const handleDrawerContentChange = (newContent: string) => { const handleDrawerContentChange = (newContent: string) => {
@ -31,4 +35,4 @@ export function RowOpenAction<T extends DataPoint>({ title, rowData, setData }:
</span> </span>
</div> </div>
); );
}; }

View File

@ -8,35 +8,31 @@ import TagsInput from "@/components/TagsInput/Index";
import Service from "@/utils/models/Service"; import Service from "@/utils/models/Service";
type ServiceTableProps = { type ServiceTableProps = {
data: Service[], data: Service[];
setData: Dispatch<SetStateAction<Service[]>> setData: Dispatch<SetStateAction<Service[]>>;
} };
/** /**
* Table componenet used for displaying services * Table componenet used for displaying services
* @param props.data Stateful list of services to be displayed by the table * @param props.data Stateful list of services to be displayed by the table
* @param props.setData State setter for the list of services * @param props.setData State setter for the list of services
*/ */
export default function ServiceTable({ data, setData }: ServiceTableProps ) { export default function ServiceTable({ data, setData }: ServiceTableProps) {
const columnHelper = createColumnHelper<Service>(); const columnHelper = createColumnHelper<Service>();
// Set up tag handling // Set up tag handling
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic",
"economic",
])
// TODO: Dynamically or statically get full list of preset requirement tag options // TODO: Dynamically or statically get full list of preset requirement tag options
const requirementProps = useTagsHandler([ const requirementProps = useTagsHandler([
'anonymous', "anonymous",
'confidential', "confidential",
'referral required', "referral required",
'safety assessment', "safety assessment",
'intake required', "intake required",
'income eligibility', "income eligibility",
'initial assessment', "initial assessment",
]) ]);
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<Service, any>[] = [ const columns: ColumnDef<Service, any>[] = [
@ -71,10 +67,7 @@ export default function ServiceTable({ data, setData }: ServiceTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...programProps} />
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
columnHelper.accessor("requirements", { columnHelper.accessor("requirements", {
@ -86,7 +79,9 @@ export default function ServiceTable({ data, setData }: ServiceTableProps ) {
cell: (info) => ( cell: (info) => (
// TODO: Setup different tag handler for requirements // TODO: Setup different tag handler for requirements
<TagsInput <TagsInput
presetValue={info.getValue()[0] !== "" ? info.getValue() : ["N/A"]} presetValue={
info.getValue()[0] !== "" ? info.getValue() : ["N/A"]
}
{...requirementProps} {...requirementProps}
/> />
), ),
@ -104,5 +99,5 @@ export default function ServiceTable({ data, setData }: ServiceTableProps ) {
}), }),
]; ];
return <Table data={data} setData={setData} columns={columns} /> return <Table data={data} setData={setData} columns={columns} />;
}; }

View File

@ -4,15 +4,15 @@ import {
useReactTable, useReactTable,
getCoreRowModel, getCoreRowModel,
flexRender, flexRender,
createColumnHelper createColumnHelper,
} from "@tanstack/react-table"; } from "@tanstack/react-table";
import { import {
ChangeEvent, ChangeEvent,
useState, useState,
useEffect, useEffect,
Key, Key,
Dispatch, Dispatch,
SetStateAction SetStateAction,
} from "react"; } from "react";
import { TableAction } from "./TableAction"; import { TableAction } from "./TableAction";
import { PlusIcon } from "@heroicons/react/24/solid"; import { PlusIcon } from "@heroicons/react/24/solid";
@ -21,9 +21,9 @@ import { RowOptionMenu } from "./RowOptionMenu";
import DataPoint from "@/utils/models/DataPoint"; import DataPoint from "@/utils/models/DataPoint";
type TableProps<T extends DataPoint> = { type TableProps<T extends DataPoint> = {
data: T[], data: T[];
setData: Dispatch<SetStateAction<T[]>>, setData: Dispatch<SetStateAction<T[]>>;
columns: ColumnDef<T, any>[] columns: ColumnDef<T, any>[];
}; };
/** Fuzzy search function */ /** Fuzzy search function */
@ -48,20 +48,21 @@ const fuzzyFilter = (
* @param props.data Stateful list of data to be held in the table * @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.setData State setter for the list of data
* @param props.columns Column definitions made with Tanstack columnHelper * @param props.columns Column definitions made with Tanstack columnHelper
*/ */
export default function Table<T extends DataPoint>({ data, setData, columns }: TableProps<T>) { export default function Table<T extends DataPoint>({
data,
setData,
columns,
}: TableProps<T>) {
const columnHelper = createColumnHelper<T>(); const columnHelper = createColumnHelper<T>();
/** Sorting function based on visibility */ /** Sorting function based on visibility */
const visibilitySort = (a: T, b: T) => ( const visibilitySort = (a: T, b: T) =>
a.visible === b.visible a.visible === b.visible ? 0 : a.visible ? -1 : 1;
? 0
: a.visible ? -1 : 1
)
// Sort data on load // Sort data on load
useEffect(() => { useEffect(() => {
setData(prevData => prevData.sort(visibilitySort)) setData((prevData) => prevData.sort(visibilitySort));
}, [setData]); }, [setData]);
// Data manipulation methods // Data manipulation methods
@ -75,13 +76,13 @@ export default function Table<T extends DataPoint>({ data, setData, columns }: T
const hideData = (dataId: number) => { const hideData = (dataId: number) => {
console.log(`Toggling visibility for data with ID: ${dataId}`); console.log(`Toggling visibility for data with ID: ${dataId}`);
setData(currentData => { setData((currentData) => {
const newData = currentData const newData = currentData
.map(data => ( .map((data) =>
data.id === dataId data.id === dataId
? { ...data, visible: !data.visible } ? { ...data, visible: !data.visible }
: data : data
)) )
.sort(visibilitySort); .sort(visibilitySort);
console.log(newData); console.log(newData);
@ -104,7 +105,7 @@ export default function Table<T extends DataPoint>({ data, setData, columns }: T
/> />
), ),
}) })
) );
// Searching // Searching
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -221,4 +222,4 @@ export default function Table<T extends DataPoint>({ data, setData, columns }: T
</table> </table>
</div> </div>
); );
}; }

View File

@ -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 { Dispatch, SetStateAction } from "react";
import useTagsHandler from "@/components/TagsInput/TagsHandler"; import useTagsHandler from "@/components/TagsInput/TagsHandler";
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"; import { ColumnDef, createColumnHelper } from "@tanstack/react-table";
@ -8,16 +12,16 @@ import TagsInput from "@/components/TagsInput/Index";
import User from "@/utils/models/User"; import User from "@/utils/models/User";
type UserTableProps = { type UserTableProps = {
data: User[], data: User[];
setData: Dispatch<SetStateAction<User[]>> setData: Dispatch<SetStateAction<User[]>>;
} };
/** /**
* Table componenet used for displaying users * Table componenet used for displaying users
* @param props.data Stateful list of users to be displayed by the table * @param props.data Stateful list of users to be displayed by the table
* @param props.setData State setter for the list of users * @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<User>(); const columnHelper = createColumnHelper<User>();
// Set up tag handling // Set up tag handling
@ -25,13 +29,9 @@ export default function UserTable({ data, setData }: UserTableProps ) {
"administrator", "administrator",
"volunteer", "volunteer",
"employee", "employee",
]) ]);
const programProps = useTagsHandler([ const programProps = useTagsHandler(["community", "domestic", "economic"]);
"community",
"domestic",
"economic",
])
// Define Tanstack columns // Define Tanstack columns
const columns: ColumnDef<User, any>[] = [ const columns: ColumnDef<User, any>[] = [
@ -57,10 +57,7 @@ export default function UserTable({ data, setData }: UserTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...roleProps} />
presetValue={info.getValue()}
{...roleProps}
/>
), ),
}), }),
columnHelper.accessor("email", { columnHelper.accessor("email", {
@ -83,13 +80,10 @@ export default function UserTable({ data, setData }: UserTableProps ) {
</> </>
), ),
cell: (info) => ( cell: (info) => (
<TagsInput <TagsInput presetValue={info.getValue()} {...programProps} />
presetValue={info.getValue()}
{...programProps}
/>
), ),
}), }),
]; ];
return <Table<User> data={data} setData={setData} columns={columns}/> return <Table<User> data={data} setData={setData} columns={columns} />;
} }

View File

@ -1,4 +1,4 @@
import { useState } from 'react'; import { useState } from "react";
/** /**
* Custom hook used to handle the state of tag options and colors * 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 tagColors.get(tag) as string;
}; };
return { presetOptions, setPresetOptions, getTagColor } return { presetOptions, setPresetOptions, getTagColor };
} }