From 4c3d49004a66b0eeaf190d1e64ca5eba85d9dbb6 Mon Sep 17 00:00:00 2001 From: Aidan Kim Date: Mon, 14 Oct 2024 18:30:35 -0400 Subject: [PATCH 1/4] Implemented API routes for getting all, creating, updating, and deleting resources, services, and tags. --- backend/api/resource.py | 69 +++-- backend/api/service.py | 76 ++++-- backend/api/tag.py | 51 ++++ backend/entities/resource_entity.py | 132 +++++----- backend/entities/resource_tag_entity.py | 90 ++++--- backend/entities/service_entity.py | 111 ++++---- backend/entities/service_tag_entity.py | 48 ++-- backend/entities/tag_entity.py | 128 +++++----- backend/services/resource.py | 325 ++++++++++++------------ backend/services/service.py | 241 +++++++++--------- backend/services/tag.py | 72 ++++-- 11 files changed, 729 insertions(+), 614 deletions(-) create mode 100644 backend/api/tag.py diff --git a/backend/api/resource.py b/backend/api/resource.py index 98dcaae..0b11539 100644 --- a/backend/api/resource.py +++ b/backend/api/resource.py @@ -1,26 +1,43 @@ -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( + subject: User, resource: Resource, resource_svc: ResourceService = Depends() +): + return resource_svc.create(subject, resource) + + +@api.get("", response_model=List[Resource], tags=["Resource"]) +def get_all(subject: User, resource_svc: ResourceService = Depends()): + return resource_svc.get_resource_by_user(subject) + + +@api.put("", response_model=Resource, tags=["Resource"]) +def update( + subject: User, resource: Resource, resource_svc: ResourceService = Depends() +): + return resource_svc.update(subject, resource) + + +@api.delete("", response_model=None, tags=["Resource"]) +def delete( + subject: User, resource: Resource, resource_svc: ResourceService = Depends() +): + resource_svc.delete(subject, resource) diff --git a/backend/api/service.py b/backend/api/service.py index 97508a9..de73d13 100644 --- a/backend/api/service.py +++ b/backend/api/service.py @@ -1,26 +1,50 @@ -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( + subject: User, + service: Service, + service_svc: ServiceService = Depends() +): + return service_svc.create(subject, service) + + +@api.get("", response_model=List[Service], tags=["Service"]) +def get_all( + subject: User, + service_svc: ServiceService = Depends() +): + return service_svc.get_service_by_user(subject) + +@api.put("", response_model=Service, tags=["Service"]) +def update( + subject: User, + service: Service, + service_svc: ServiceService = Depends() +): + return service_svc.update(subject, service) + +@api.delete("", response_model=None, tags=["Service"]) +def delete( + subject: User, + service: Service, + service_svc: ServiceService = Depends() +): + service_svc.delete(subject, service) diff --git a/backend/api/tag.py b/backend/api/tag.py new file mode 100644 index 0000000..da6256f --- /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 +): + return tag_service.create(subject, tag) + +@api.get("", response_model=List[Tag], tags=["Tag"]) +def get_all( + subject: User, + tag_svc: TagService +): + return tag_svc.get_all() + +@api.put("", response_model=Tag, tags=["Tag"]) +def delete( + subject: User, + tag: Tag, + tag_svc: TagService +): + return tag_svc.delete(subject, tag) + +@api.delete("", response_model=None, tags=["Tag"]) +def delete( + subject: User, + tag: Tag, + tag_svc: TagService +): + tag_svc.delete(subject, tag) diff --git a/backend/entities/resource_entity.py b/backend/entities/resource_entity.py index 299decb..c510bf4 100644 --- a/backend/entities/resource_entity.py +++ b/backend/entities/resource_entity.py @@ -1,67 +1,65 @@ -""" 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 ForeignKey, 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 + tags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="resource") + + @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, + ) diff --git a/backend/entities/resource_tag_entity.py b/backend/entities/resource_tag_entity.py index e6d863b..6ae5d1b 100644 --- a/backend/entities/resource_tag_entity.py +++ b/backend/entities/resource_tag_entity.py @@ -1,46 +1,44 @@ -""" 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) + resource_id: Mapped[int] = mapped_column(ForeignKey("resource.id"), primary_key=True) + tag_id: Mapped[int] = mapped_column(ForeignKey("tag.id", primary_key=True)) + resource: Mapped["ResourceEntity"] = mapped_column(backpopulates="tags") + tag: Mapped["TagEntity"] = mapped_column(backpopulates="resource_tags") + + # @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, + # ) diff --git a/backend/entities/service_entity.py b/backend/entities/service_entity.py index 4f1f9fc..63b2093 100644 --- a/backend/entities/service_entity.py +++ b/backend/entities/service_entity.py @@ -1,47 +1,64 @@ -""" 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 +""" 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 + tags: 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, + ) diff --git a/backend/entities/service_tag_entity.py b/backend/entities/service_tag_entity.py index 0f05738..7896e56 100644 --- a/backend/entities/service_tag_entity.py +++ b/backend/entities/service_tag_entity.py @@ -1,25 +1,23 @@ -""" 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) + resource_id: Mapped[int] = mapped_column(ForeignKey("service.id"), primary_key=True) + tag_id: Mapped[int] = mapped_column(ForeignKey("tag.id", primary_key=True)) + service: Mapped["ServiceEntity"] = mapped_column(backpopulates="tags") + tag: Mapped["TagEntity"] = mapped_column(backpopulates="service_tags") diff --git a/backend/entities/tag_entity.py b/backend/entities/tag_entity.py index 0d1548b..b42616d 100644 --- a/backend/entities/tag_entity.py +++ b/backend/entities/tag_entity.py @@ -1,65 +1,63 @@ -""" 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) + resource_tags: Mapped[list["ResourceTagEntity"]] = relationship( + back_populates="tag" + ) + service_tags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag") + + @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, + created_at=model.created_at + ) + + 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, + created_at=self.created_at + ) diff --git a/backend/services/resource.py b/backend/services/resource.py index 2648ad6..2ccbee9 100644 --- a/backend/services/resource.py +++ b/backend/services/resource.py @@ -1,165 +1,160 @@ -from fastapi import Depends -from ..database import db_session -from sqlalchemy.orm import Session -from sqlalchemy import select -from ..models.resource_model import Resource -from ..entities.resource_entity import ResourceEntity -from ..models.user_model import User, UserTypeEnum - -from .exceptions import ResourceNotFoundException - - -class ResourceService: - - def __init__(self, session: Session = Depends(db_session)): - self._session = session - - def get_resource_by_user(self, subject: User): - """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() - for entity in entities: - resources.append(entity) - - return [resource.to_model() for resource in resources] - - def create(self, user: 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: - user: a valid User model representing the currently logged in User - resource: Resource object to add to table - - Returns: - Resource: Object added to table - """ - if resource.role != user.role or resource.group != user.group: - raise PermissionError( - "User does not have permission to add resources in this role or group." - ) - - 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.role == user.role, - ResourceEntity.group == user.group, - ) - .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 - - Parameters: - user: a valid User model representing the currently logged in User - resource (ResourceEntity): Resource to update - - Returns: - Resource: Updated resource object - - Raises: - ResourceNotFoundException: If no resource is found with the corresponding ID - """ - if resource.role != user.role or resource.group != user.group: - raise PermissionError( - "User does not have permission to update this resource." - ) - - obj = self._session.get(ResourceEntity, resource.id) if resource.id else None - - if obj is None: - raise ResourceNotFoundException( - f"No resource found with matching id: {resource.id}" - ) - - obj.update_from_model(resource) # Assuming an update method exists - self._session.commit() - - return obj.to_model() - - def delete(self, user: User, id: int) -> None: - """ - Delete resource based on id that the user has access to - - Parameters: - user: a valid User model representing the currently logged in User - id: int, a unique resource id - - Raises: - ResourceNotFoundException: If no resource is found with the corresponding id - """ - resource = ( - self._session.query(ResourceEntity) - .filter( - ResourceEntity.id == id, - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, - ) - .one_or_none() - ) - - if resource is None: - raise ResourceNotFoundException(f"No resource found with matching id: {id}") - - self._session.delete(resource) - self._session.commit() - - def get_by_slug(self, user: User, search_string: str) -> list[Resource]: - """ - Get a list of resources given a search string that the user has access to - - Parameters: - user: a valid User model representing the currently logged in User - search_string: a string to search resources by - - Returns: - list[Resource]: list of resources relating to the string - - Raises: - ResourceNotFoundException if no resource is found with the corresponding slug - """ - query = select(ResourceEntity).where( - ResourceEntity.title.ilike(f"%{search_string}%"), - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, - ) - entities = self._session.scalars(query).all() - - return [entity.to_model() for entity in entities] +from fastapi import Depends +from ..database import db_session +from sqlalchemy.orm import Session +from sqlalchemy import select +from ..models.resource_model import Resource +from ..entities.resource_entity import ResourceEntity +from ..models.user_model import User, UserTypeEnum + +from .exceptions import ResourceNotFoundException + + +class ResourceService: + + def __init__(self, session: Session = Depends(db_session)): + self._session = session + + 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: + entities = self._session.query(ResourceEntity).where(ResourceEntity.program == program).all() + for entity in entities: + resources.append(entity.to_model()) + return [resource for resource in resources] + + 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: + user: a valid User model representing the currently logged in User + resource: Resource object to add to table + + Returns: + Resource: Object added to table + """ + # Ask about what the requirements for making a resource are. + if resource.role != subject.role or resource.group != subject.group: + raise PermissionError( + "User does not have permission to add resources in this role or group." + ) + + 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.role == user.role, + ResourceEntity.group == user.group, + ) + .one_or_none() + ) + + if resource is None: + raise ResourceNotFoundException(f"No resource found with id: {id}") + + return resource.to_model() + + def update(self, subject: User, resource: Resource) -> Resource: + """ + Update the resource if the user has access + + Parameters: + user: a valid User model representing the currently logged in User + resource (ResourceEntity): Resource to update + + Returns: + Resource: Updated resource object + + Raises: + ResourceNotFoundException: If no resource is found with the corresponding ID + """ + if resource.role != subject.role or resource.group != subject.group: + raise PermissionError( + "User does not have permission to update this resource." + ) + + 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}" + ) + + entity.name = resource.name + entity.summary = resource.summary + entity.link = resource.link + entity.program = resource.program + self._session.commit() + + return entity.to_model() + + def delete(self, subject: User, resource: Resource) -> None: + """ + Delete resource based on id that the user has access to + + Parameters: + user: a valid User model representing the currently logged in User + id: int, a unique resource id + + Raises: + ResourceNotFoundException: If no resource is found with the corresponding 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}") + + self._session.delete(entity) + self._session.commit() + + def get_by_slug(self, user: User, search_string: str) -> list[Resource]: + """ + Get a list of resources given a search string that the user has access to + + Parameters: + user: a valid User model representing the currently logged in User + search_string: a string to search resources by + + Returns: + list[Resource]: list of resources relating to the string + + Raises: + ResourceNotFoundException if no resource is found with the corresponding slug + """ + query = select(ResourceEntity).where( + ResourceEntity.title.ilike(f"%{search_string}%"), + ResourceEntity.role == user.role, + ResourceEntity.group == user.group, + ) + entities = self._session.scalars(query).all() + + return [entity.to_model() for entity in entities] diff --git a/backend/services/service.py b/backend/services/service.py index f8afe2e..edce109 100644 --- a/backend/services/service.py +++ b/backend/services/service.py @@ -1,127 +1,114 @@ -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 + resources = [] + for program in programs: + entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all() + for entity in entities: + resources.append(entity.to_model()) + return [service for service in resources] + + 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 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() + From a563ca2bfa9bc982945081e76963cf6e52fd222b Mon Sep 17 00:00:00 2001 From: Aidan Kim Date: Tue, 15 Oct 2024 19:49:47 -0400 Subject: [PATCH 2/4] Updated main.py for API routes to include tags and rolled entities back. --- backend/api/tag.py | 10 ++-- backend/entities/resource_entity.py | 8 +-- backend/entities/resource_tag_entity.py | 12 +++-- backend/entities/service_entity.py | 27 ++-------- backend/entities/service_tag_entity.py | 11 ++-- backend/entities/tag_entity.py | 23 ++++---- backend/main.py | 71 +++++++++++++------------ 7 files changed, 78 insertions(+), 84 deletions(-) diff --git a/backend/api/tag.py b/backend/api/tag.py index da6256f..36e7e4a 100644 --- a/backend/api/tag.py +++ b/backend/api/tag.py @@ -23,22 +23,22 @@ openapi_tags = { def create( subject: User, tag: Tag, - tag_service: TagService + 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 + tag_svc: TagService=Depends() ): return tag_svc.get_all() @api.put("", response_model=Tag, tags=["Tag"]) -def delete( +def update( subject: User, tag: Tag, - tag_svc: TagService + tag_svc: TagService=Depends() ): return tag_svc.delete(subject, tag) @@ -46,6 +46,6 @@ def delete( def delete( subject: User, tag: Tag, - tag_svc: TagService + tag_svc: TagService=Depends() ): tag_svc.delete(subject, tag) diff --git a/backend/entities/resource_entity.py b/backend/entities/resource_entity.py index c510bf4..208929b 100644 --- a/backend/entities/resource_entity.py +++ b/backend/entities/resource_entity.py @@ -1,7 +1,7 @@ """ Defines the table for storing resources """ # Import our mapped SQL types from SQLAlchemy -from sqlalchemy import ForeignKey, Integer, String, DateTime, Enum +from sqlalchemy import Integer, String, DateTime, Enum # Import mapping capabilities from the SQLAlchemy ORM from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -31,7 +31,9 @@ class ResourceEntity(EntityBase): link: Mapped[str] = mapped_column(String, nullable=False) program: Mapped[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) # relationships - tags: Mapped[list["ResourceTagEntity"]] = relationship(back_populates="resource") + resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( + back_populates="resource", cascade="all,delete" + ) @classmethod def from_model(cls, model: Resource) -> Self: @@ -62,4 +64,4 @@ class ResourceEntity(EntityBase): 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 6ae5d1b..53a9323 100644 --- a/backend/entities/resource_tag_entity.py +++ b/backend/entities/resource_tag_entity.py @@ -23,10 +23,12 @@ class ResourceTagEntity(EntityBase): # set fields or 'columns' for the user table id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - resource_id: Mapped[int] = mapped_column(ForeignKey("resource.id"), primary_key=True) - tag_id: Mapped[int] = mapped_column(ForeignKey("tag.id", primary_key=True)) - resource: Mapped["ResourceEntity"] = mapped_column(backpopulates="tags") - tag: Mapped["TagEntity"] = mapped_column(backpopulates="resource_tags") + 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: @@ -41,4 +43,4 @@ class ResourceTagEntity(EntityBase): # 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 63b2093..809f5d9 100644 --- a/backend/entities/service_entity.py +++ b/backend/entities/service_entity.py @@ -20,7 +20,6 @@ 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 @@ -33,32 +32,16 @@ class ServiceEntity(EntityBase): 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 - ) + program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) # relationships - tags: Mapped[list["ServiceTagEntity"]] = relationship( + 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, - ) + 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, - ) + 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 7896e56..6d4e908 100644 --- a/backend/entities/service_tag_entity.py +++ b/backend/entities/service_tag_entity.py @@ -17,7 +17,10 @@ class ServiceTagEntity(EntityBase): # set fields or 'columns' for the user table id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) - resource_id: Mapped[int] = mapped_column(ForeignKey("service.id"), primary_key=True) - tag_id: Mapped[int] = mapped_column(ForeignKey("tag.id", primary_key=True)) - service: Mapped["ServiceEntity"] = mapped_column(backpopulates="tags") - tag: Mapped["TagEntity"] = mapped_column(backpopulates="service_tags") + 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 b42616d..3916798 100644 --- a/backend/entities/tag_entity.py +++ b/backend/entities/tag_entity.py @@ -16,21 +16,22 @@ from ..models.tag_model import Tag from typing import Self - class TagEntity(EntityBase): - # set table name + #set table name __tablename__ = "tag" - # set fields + #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) - resource_tags: Mapped[list["ResourceTagEntity"]] = relationship( - back_populates="tag" - ) - service_tags: Mapped[list["ServiceTagEntity"]] = relationship(back_populates="tag") + #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: """ @@ -41,11 +42,11 @@ class TagEntity(EntityBase): Returns: self: The entity """ - + return cls( id=model.id, + created_at=model.created_at, content=model.id, - created_at=model.created_at ) def to_model(self) -> Tag: @@ -58,6 +59,6 @@ class TagEntity(EntityBase): return Tag( id=self.id, + create_at=self.created_at, content=self.content, - created_at=self.created_at - ) + ) \ No newline at end of file diff --git a/backend/main.py b/backend/main.py index d8af3ff..5720633 100644 --- a/backend/main.py +++ b/backend/main.py @@ -1,34 +1,37 @@ -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)}) From 3814d6d290dc70f6993e77fe3c27a718488f793e Mon Sep 17 00:00:00 2001 From: Aidan Kim Date: Tue, 29 Oct 2024 19:24:07 -0400 Subject: [PATCH 3/4] Created API routes for create, update, delete, get_all, and get_by_name. Deleted service methods for get by id. --- backend/api/resource.py | 21 +++++-- backend/api/service.py | 26 ++++---- backend/main.py | 1 - backend/services/exceptions.py | 2 + backend/services/resource.py | 105 +++++++++++---------------------- backend/services/service.py | 33 ++++------- 6 files changed, 78 insertions(+), 110 deletions(-) diff --git a/backend/api/resource.py b/backend/api/resource.py index 0b11539..97d25af 100644 --- a/backend/api/resource.py +++ b/backend/api/resource.py @@ -19,25 +19,38 @@ openapi_tags = { # TODO: Create custom exceptions @api.post("", response_model=Resource, tags=["Resource"]) def create( - subject: User, resource: Resource, resource_svc: ResourceService = Depends() + uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() ): + subject = user_svc.get_user_by_uuid(uuid) return resource_svc.create(subject, resource) @api.get("", response_model=List[Resource], tags=["Resource"]) -def get_all(subject: User, resource_svc: ResourceService = Depends()): +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( - subject: User, resource: Resource, resource_svc: ResourceService = Depends() + uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() ): + subject = user_svc.get_user_by_uuid(uuid) return resource_svc.update(subject, resource) @api.delete("", response_model=None, tags=["Resource"]) def delete( - subject: User, resource: Resource, resource_svc: ResourceService = Depends() + uuid: str, resource: Resource, user_svc: UserService = Depends(), resource_svc: ResourceService = Depends() ): + subject = user_svc.get_user_by_uuid(uuid) resource_svc.delete(subject, resource) diff --git a/backend/api/service.py b/backend/api/service.py index de73d13..bd3c4dc 100644 --- a/backend/api/service.py +++ b/backend/api/service.py @@ -19,32 +19,36 @@ openapi_tags = { # TODO: Create custom exceptions @api.post("", response_model=Service, tags=["Service"]) def create( - subject: User, - service: Service, - service_svc: ServiceService = Depends() + 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( - subject: User, - service_svc: ServiceService = Depends() + 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( - subject: User, - service: Service, - service_svc: ServiceService = Depends() + 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( - subject: User, - service: Service, - service_svc: ServiceService = Depends() + 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/main.py b/backend/main.py index 5720633..12ecaad 100644 --- a/backend/main.py +++ b/backend/main.py @@ -3,7 +3,6 @@ from fastapi.responses import JSONResponse from fastapi.middleware.gzip import GZipMiddleware - from .api import user, health, service, resource, tag description = """ 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 2ccbee9..3522003 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: @@ -24,11 +24,29 @@ class ResourceService: programs = subject.program resources = [] for program in programs: - entities = self._session.query(ResourceEntity).where(ResourceEntity.program == program).all() + entities = ( + self._session.query(ResourceEntity) + .where(ResourceEntity.program == program) + .all() + ) for entity in entities: resources.append(entity.to_model()) return [resource 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, 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. @@ -40,47 +58,15 @@ class ResourceService: Returns: Resource: Object added to table """ - # Ask about what the requirements for making a resource are. - if resource.role != subject.role or resource.group != subject.group: - raise PermissionError( - "User does not have permission to add resources in this role or group." + 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.role == user.role, - ResourceEntity.group == user.group, - ) - .one_or_none() - ) - - if resource is None: - raise ResourceNotFoundException(f"No resource found with id: {id}") - - return resource.to_model() - def update(self, subject: User, resource: Resource) -> Resource: """ Update the resource if the user has access @@ -95,25 +81,21 @@ class ResourceService: Raises: ResourceNotFoundException: If no resource is found with the corresponding ID """ - if resource.role != subject.role or resource.group != subject.group: - 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" ) - 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}" ) - entity.name = resource.name entity.summary = resource.summary entity.link = resource.link entity.program = resource.program self._session.commit() - return entity.to_model() def delete(self, subject: User, resource: Resource) -> None: @@ -127,34 +109,15 @@ class ResourceService: Raises: ResourceNotFoundException: If no resource is found with the corresponding id """ + if subject.role != UserTypeEnum.ADMIN: + raise ProgramNotAssignedException( + f"User is not {UserTypeEnum.ADMIN}, cannot update service" + ) 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}") - + raise ResourceNotFoundException( + f"No resource found with matching id: {resource.id}" + ) self._session.delete(entity) self._session.commit() - - def get_by_slug(self, user: User, search_string: str) -> list[Resource]: - """ - Get a list of resources given a search string that the user has access to - - Parameters: - user: a valid User model representing the currently logged in User - search_string: a string to search resources by - - Returns: - list[Resource]: list of resources relating to the string - - Raises: - ResourceNotFoundException if no resource is found with the corresponding slug - """ - query = select(ResourceEntity).where( - ResourceEntity.title.ilike(f"%{search_string}%"), - ResourceEntity.role == user.role, - ResourceEntity.group == user.group, - ) - entities = self._session.scalars(query).all() - - return [entity.to_model() for entity in entities] diff --git a/backend/services/service.py b/backend/services/service.py index edce109..e10309e 100644 --- a/backend/services/service.py +++ b/backend/services/service.py @@ -27,37 +27,24 @@ class ServiceService: return [service.to_model() for service in entities] else: programs = subject.program - resources = [] + services = [] for program in programs: entities = self._session.query(ServiceEntity).where(ServiceEntity.program == program).all() for entity in entities: - resources.append(entity.to_model()) - return [service for service in resources] + services.append(entity.to_model()) + return [service for service in services] - 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: + def get_service_by_name(self, name: str, subject: User) -> Service: """Service method getting services by id.""" - query = select(ServiceEntity).filter(ServiceEntity.id == 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 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") + raise ServiceNotFoundException(f"Service with name: {name} does not exist or program has not been assigned") return entity.to_model() From 638f1098962e68c3e3419ac2e8dbe235cf2216b9 Mon Sep 17 00:00:00 2001 From: pmoharana-cmd Date: Mon, 4 Nov 2024 16:25:47 -0500 Subject: [PATCH 4/4] Defaults created_at to current time --- backend/models/resource_model.py | 2 +- backend/models/service_model.py | 2 +- backend/models/tag_model.py | 2 +- backend/models/user_model.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) 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