From a10733773738b98a9e9a5641533e0be992bfb7fc Mon Sep 17 00:00:00 2001 From: Aidan Kim Date: Sun, 21 Apr 2024 19:14:00 -0400 Subject: [PATCH] updated the service methods and modified a few of the tests. --- backend/entities/sample_entity.py | 12 ++ backend/entities/service_entity.py | 15 ++- backend/entities/user_entity.py | 9 +- backend/services/exceptions.py | 7 + backend/services/service.py | 87 +++++++++++++ backend/services/user.py | 3 +- backend/test/services/fixtures.py | 6 + backend/test/services/service_test.py | 63 +++++++++ backend/test/services/service_test_data.py | 143 +++++++++++++++++++++ backend/test/services/user_test.py | 3 +- backend/test/services/user_test_data.py | 2 +- 11 files changed, 340 insertions(+), 10 deletions(-) create mode 100644 backend/entities/sample_entity.py create mode 100644 backend/services/exceptions.py create mode 100644 backend/test/services/service_test_data.py diff --git a/backend/entities/sample_entity.py b/backend/entities/sample_entity.py new file mode 100644 index 0000000..df74d1a --- /dev/null +++ b/backend/entities/sample_entity.py @@ -0,0 +1,12 @@ +from sqlalchemy import create_engine, Column, Integer, String +from sqlalchemy.orm import Mapped, mapped_column, relationship +from .entity_base import EntityBase + + +class SampleEntity(EntityBase): + __tablename__ = "persons" + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + name: Mapped[str] = mapped_column(String, nullable=False) + age: Mapped[int] = mapped_column(Integer) + email: Mapped[str] = mapped_column(String, unique=True, nullable=False) \ No newline at end of file diff --git a/backend/entities/service_entity.py b/backend/entities/service_entity.py index b6f999a..4f1f9fc 100644 --- a/backend/entities/service_entity.py +++ b/backend/entities/service_entity.py @@ -13,9 +13,12 @@ from .entity_base import EntityBase from datetime import datetime # Import enums for Program -from .program_enum import Program_Enum +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): @@ -26,11 +29,19 @@ class ServiceEntity(EntityBase): 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[Program_Enum] = mapped_column(Enum(Program_Enum), nullable=False) + program: Mapped[ProgramTypeEnum] = mapped_column(Enum(ProgramTypeEnum), nullable=False) # relationships serviceTags: Mapped[list["ServiceTagEntity"]] = relationship( back_populates="service", cascade="all,delete" ) + + def to_model(self) -> Service: + return Service(id=self.id, name=self.name, status=self.status, summary=self.summary, requirements=self.requirements, program=self.program) + + @classmethod + def from_model(cls, model:Service) -> Self: + return cls(id=model.id, name=model.name, status=model.status, summary=model.summary, requirements=model.requirements, program=model.program) \ No newline at end of file diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index 9534746..ca66ee9 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -13,8 +13,7 @@ from .entity_base import EntityBase from datetime import datetime # Import enums for Role and Program -from .program_enum import Program_Enum -from .user_enum import Role_Enum +from backend.models.enum_for_models import UserTypeEnum, ProgramTypeEnum # Import models for User methods from ..models.user_model import User @@ -34,10 +33,10 @@ class UserEntity(EntityBase): username: Mapped[str] = mapped_column( String(32), nullable=False, default="", unique=True ) - role: Mapped[Role_Enum] = mapped_column(Enum(Role_Enum), nullable=False) + role: Mapped[UserTypeEnum] = mapped_column(Enum(UserTypeEnum), nullable=False) email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True) - program: Mapped[list[Program_Enum]] = mapped_column( - ARRAY(Enum(Program_Enum)), nullable=False + program: Mapped[list[ProgramTypeEnum]] = mapped_column( + ARRAY(Enum(ProgramTypeEnum)), nullable=False ) experience: Mapped[int] = mapped_column(Integer, nullable=False) group: Mapped[str] = mapped_column(String(50)) diff --git a/backend/services/exceptions.py b/backend/services/exceptions.py new file mode 100644 index 0000000..6a78e49 --- /dev/null +++ b/backend/services/exceptions.py @@ -0,0 +1,7 @@ +class ServiceNotFoundException(Exception): + """Exception for when the service being requested is not in the table.""" + ... + +class ProgramNotAssignedException(Exception): + """Exception for when the user does not have correct access for requested services.""" + ... \ No newline at end of file diff --git a/backend/services/service.py b/backend/services/service.py index 28a9e3c..badcea1 100644 --- a/backend/services/service.py +++ b/backend/services/service.py @@ -1,9 +1,96 @@ 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_user(self, subject: User): + """Service method getting all of the services that a user has access to based on role""" + programs = subject.program + services = [] + for program in programs: + query = select(ServiceEntity).filter(ServiceEntity.program == program) + entities = self._session.scalars(query) + all.append(entities) + 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.ADMIN: + raise ProgramNotAssignedException(f"User is not {UserTypeEnum.ADMIN}, 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() \ No newline at end of file diff --git a/backend/services/user.py b/backend/services/user.py index be01e00..360db01 100644 --- a/backend/services/user.py +++ b/backend/services/user.py @@ -61,7 +61,8 @@ class UserService: """ try: - user = self.get_user_by_id(user.id) + if (user.id != None): + user = self.get_user_by_id(user.id) except: # if does not exist, create new object user_entity = UserEntity.from_model(user) diff --git a/backend/test/services/fixtures.py b/backend/test/services/fixtures.py index e6fd67b..9fb349a 100644 --- a/backend/test/services/fixtures.py +++ b/backend/test/services/fixtures.py @@ -5,6 +5,7 @@ from unittest.mock import create_autospec from sqlalchemy.orm import Session from ...services import UserService from ...services import TagService +from ...services import ServiceService @@ -18,3 +19,8 @@ def user_svc(session: Session): def tag_svc(session: Session): """This fixture is used to test the TagService class""" return TagService(session) + +@pytest.fixture() +def service_svc(session: Session): + """This fixture is used to test the ServiceService class""" + return ServiceService(session) \ No newline at end of file diff --git a/backend/test/services/service_test.py b/backend/test/services/service_test.py index e69de29..18356ea 100644 --- a/backend/test/services/service_test.py +++ b/backend/test/services/service_test.py @@ -0,0 +1,63 @@ +import pytest + +from ...services import ServiceService +from .fixtures import service_svc +from ...models.enum_for_models import ProgramTypeEnum + +from . import user_test_data +from . import service_test_data +from ...services.exceptions import ServiceNotFoundException, ProgramNotAssignedException + +def test_get_all(service_svc: ServiceService): + service = service_svc.get_all(user_test_data.admin) + assert len(service) == len(service_test_data.services) + + +def test_get_by_id(service_svc: ServiceService): + if service_test_data.service_1.id != None: + service = service_svc.get_service_by_id(service_test_data.service_1.id) + assert service.id == service_test_data.service_1.id + + +def test_get_by_name_not_found(service_svc: ServiceService): + with pytest.raises(ServiceNotFoundException): + service_svc.get_service_by_id(12) + pytest.fail() + + +def test_get_by_program(service_svc: ServiceService): + services = service_svc.get_service_by_program(ProgramTypeEnum.COMMUNITY) + for service in services: + assert service.program == ProgramTypeEnum.COMMUNITY + assert isinstance(service, Service) + + +def test_create(service_svc: ServiceService): + service = service_svc.create(user_test_data.admin, service_test_data.service_7) + assert service.name == service_test_data.service_7.name + assert isinstance(service, Service) + + +def test_update(service_svc: ServiceService): + service = service_svc.update(user_test_data.admin, service_test_data.service_6_edit) + assert service.status == service_test_data.service_6_edit.status + assert service.requirements == service_test_data.service_6_edit.requirements + assert isinstance(service, Service) + + +def test_update_not_found(service_svc: ServiceService): + with pytest.raises(ServiceNotFoundException): + service = service_svc.update(user_test_data.admin, service_test_data.new_service) + pytest.fail() + + +def test_delete(service_svc: ServiceService): + service_svc.delete(user_test_data.admin, service_test_data.service_1) + services = service_svc.get_all(user_test_data.admin) + assert len(services) == len(service_test_data.services) - 1 + + +def test_delete_not_found(service_svc: ServiceService): + with pytest.raises(ServiceNotFoundException): + service_svc.delete(user_test_data.admin, service_test_data.service_7) + pytest.fail() \ No newline at end of file diff --git a/backend/test/services/service_test_data.py b/backend/test/services/service_test_data.py new file mode 100644 index 0000000..f3828a0 --- /dev/null +++ b/backend/test/services/service_test_data.py @@ -0,0 +1,143 @@ +import pytest +from sqlalchemy.orm import Session +from backend.entities.service_entity import ServiceEntity +from backend.models.service_model import Service +from backend.models.enum_for_models import ProgramTypeEnum + +service_1 = Service( + id = 1, + name = "service 1", + status= "open", + summary= "presentation educating community on domestic violence", + requirements= [""], + program= ProgramTypeEnum.COMMUNITY +) + +service_2 = Service( + id = 2, + name = "service 2", + status= "closed", + summary= "service finding safe places to stay", + requirements= [""], + program= ProgramTypeEnum.DOMESTIC +) + +service_3 = Service( + id = 3, + name = "service 3", + status= "open", + summary= "", + requirements= [""], + program= ProgramTypeEnum.DOMESTIC +) + +service_4 = Service( + id = 4, + name = "service 4", + status= "waitlist", + summary= "community event", + requirements= [""], + program= ProgramTypeEnum.COMMUNITY +) + +service_5 = Service( + id = 5, + name = "service 5", + status= "open", + summary= "talk circle for victims of domestic violence", + requirements= ["18+"], + program= ProgramTypeEnum.COMMUNITY +) + +service_6 = Service( + id = 6, + name = "service 6", + status= "waitlist", + summary= "program offering economic assistance", + requirements= [""], + program= ProgramTypeEnum.ECONOMIC +) + +service_6_edit = Service( + id = 6, + name = "service 6", + status= "open", + summary= "program offering economic assistance", + requirements= ["18+"], + program= ProgramTypeEnum.ECONOMIC +) + +service_7 = Service( + id = 7, + name = "service 7", + status= "waitlist", + summary= "insert generic description", + requirements= [""], + program= ProgramTypeEnum.ECONOMIC +) + +new_service = Service( + id = 8, + name = "new service", + status= "open", + summary= "insert other generic description", + requirements= [""], + program= ProgramTypeEnum.DOMESTIC +) + +services = [service_1, service_2, service_3, service_4, service_5, service_6] + +from sqlalchemy import text +from sqlalchemy.orm import Session, DeclarativeBase, InstrumentedAttribute + + +def reset_table_id_seq( + session: Session, + entity: type[DeclarativeBase], + entity_id_column: InstrumentedAttribute[int], + next_id: int, +) -> None: + """Reset the ID sequence of an entity table. + + Args: + session (Session) - A SQLAlchemy Session + entity (DeclarativeBase) - The SQLAlchemy Entity table to target + entity_id_column (MappedColumn) - The ID column (should be an int column) + next_id (int) - Where the next inserted, autogenerated ID should begin + + Returns: + None""" + table = entity.__table__ + id_column_name = entity_id_column.name + sql = text(f"ALTER SEQUENCe {table}_{id_column_name}_seq RESTART WITH {next_id}") + session.execute(sql) + + +def insert_fake_data(session: Session): + """Inserts fake organization data into the test session.""" + + global services + + # Create entities for test organization data + entities = [] + for service in services: + entity = ServiceEntity.from_model(service) + session.add(entity) + entities.append(entity) + + # Reset table IDs to prevent ID conflicts + reset_table_id_seq(session, ServiceEntity, ServiceEntity.id, len(services) + 1) + + # Commit all changes + session.commit() + + +@pytest.fixture(autouse=True) +def fake_data_fixture(session: Session): + """Insert fake data the session automatically when test is run. + Note: + This function runs automatically due to the fixture property `autouse=True`. + """ + insert_fake_data(session) + session.commit() + yield \ No newline at end of file diff --git a/backend/test/services/user_test.py b/backend/test/services/user_test.py index af91294..49a49bc 100644 --- a/backend/test/services/user_test.py +++ b/backend/test/services/user_test.py @@ -36,7 +36,8 @@ def test_get_all(user_svc: UserService): def test_get_user_by_id(user_svc: UserService): """Test getting a user by an id""" - user = user_svc.get_user_by_id(volunteer.id) + if volunteer.id != None: + user = user_svc.get_user_by_id(volunteer.id) assert user is not None assert user.id is not None diff --git a/backend/test/services/user_test_data.py b/backend/test/services/user_test_data.py index 65e2ff5..d975f72 100644 --- a/backend/test/services/user_test_data.py +++ b/backend/test/services/user_test_data.py @@ -191,6 +191,6 @@ def fake_data_fixture(session: Session): Note: This function runs automatically due to the fixture property `autouse=True`. """ - # insert_fake_data(session) + insert_fake_data(session) session.commit() yield