diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..7581cbf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "backend" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/backend/entities/__init__.py b/backend/entities/__init__.py index 0b82067..5bb11e6 100644 --- a/backend/entities/__init__.py +++ b/backend/entities/__init__.py @@ -1,7 +1,7 @@ from .entity_base import EntityBase from .sample_entity import SampleEntity from .tag_entity import TagEntity -from .user_entity import UserEntity +#from .user_entity import UserEntity from .resource_entity import ResourceEntity from .resource_tag_entity import ResourceTagEntity from .service_entity import ServiceEntity diff --git a/backend/entities/program_enum.py b/backend/entities/program_enum.py index 3a207bd..a869116 100644 --- a/backend/entities/program_enum.py +++ b/backend/entities/program_enum.py @@ -2,9 +2,9 @@ from sqlalchemy import Enum class ProgramEnum(Enum): - ECONOMIC = "economic" - DOMESTIC = "domestic" - COMMUNITY = "community" + ECONOMIC = 'ECONOMIC' + DOMESTIC = 'DOMESTIC' + COMMUNITY = 'COMMUNITY' def __init__(self): super().__init__(name="program_enum") diff --git a/backend/entities/resource_entity.py b/backend/entities/resource_entity.py index b38e625..2cfb1ff 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 Integer, String, DateTime +from sqlalchemy import Integer, String, DateTime, ARRAY # Import mapping capabilities from the SQLAlchemy ORM from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -28,8 +28,7 @@ class ResourceEntity(EntityBase): name: Mapped[str] = mapped_column(String(32), nullable=False) summary: Mapped[str] = mapped_column(String(100), nullable=False) link: Mapped[str] = mapped_column(String, nullable=False) - program: Mapped[ProgramEnum] = mapped_column(ProgramEnum, nullable=False) - + program: Mapped[list[ProgramEnum]] = mapped_column(ARRAY(ProgramEnum), nullable=False) # relationships resourceTags: Mapped[list["ResourceTagEntity"]] = relationship( back_populates="resource", cascade="all,delete" diff --git a/backend/entities/service_tag_entity.py b/backend/entities/service_tag_entity.py index c1dbdc7..b04d2d4 100644 --- a/backend/entities/service_tag_entity.py +++ b/backend/entities/service_tag_entity.py @@ -21,5 +21,5 @@ class ServiceTagEntity(EntityBase): tagId: Mapped[int] = mapped_column(ForeignKey("tag.id")) # relationships - service: Mapped["ServiceEntity"] = relationship(back_populates="resourceTags") - tag: Mapped["TagEntity"] = relationship(back_populates="resourceTags") + service: Mapped["ServiceEntity"] = relationship(back_populates="serviceTags") + tag: Mapped["TagEntity"] = relationship(back_populates="serviceTags") diff --git a/backend/entities/tag_entity.py b/backend/entities/tag_entity.py index e61f1ee..0d1548b 100644 --- a/backend/entities/tag_entity.py +++ b/backend/entities/tag_entity.py @@ -12,6 +12,10 @@ 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 @@ -27,17 +31,17 @@ class TagEntity(EntityBase): 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, @@ -45,18 +49,17 @@ class TagEntity(EntityBase): ) 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.id, + content=self.content, ) - """ diff --git a/backend/entities/user_entity.py b/backend/entities/user_entity.py index 859b0dc..435023c 100644 --- a/backend/entities/user_entity.py +++ b/backend/entities/user_entity.py @@ -17,7 +17,7 @@ from datetime import datetime # Import enums for Role and Program -from backend.entities.program_enum import ProgramEnum +from .program_enum import ProgramEnum from .user_enum import RoleEnum #Import models for User methods @@ -36,9 +36,9 @@ class UserEntity(EntityBase): id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.now) username: Mapped[str] = mapped_column(String(32), nullable=False, default="", unique=True ) - role: Mapped[RoleEnum] = mapped_column(RoleEnum, nullable=False) + role: Mapped[RoleEnum] = mapped_column(String, nullable=False) email: Mapped[str] = mapped_column(String(50), nullable=False, unique=True) - program: Mapped[list[ProgramEnum]] = mapped_column(ARRAY(ProgramEnum), nullable=False) + program: Mapped[list[str]] = mapped_column(ARRAY(String), nullable=False) experience: Mapped[int] = mapped_column(Integer, nullable=False) group: Mapped[str] = mapped_column(String(50)) @@ -56,13 +56,13 @@ class UserEntity(EntityBase): return cls( id=model.id, - username=model.username, - email=model.email, - experience=model.experience, - group=model.group, - program=model.programtype, - role=model.usertype, created_at=model.created_at, + username=model.username, + role=model.usertype, + email=model.email, + program=model.programtype, + experience=model.experience, + group=model.group, ) def to_model(self) -> User: diff --git a/backend/entities/user_enum.py b/backend/entities/user_enum.py index 99594ec..18f034f 100644 --- a/backend/entities/user_enum.py +++ b/backend/entities/user_enum.py @@ -4,9 +4,9 @@ from sqlalchemy import Enum class RoleEnum(Enum): """Determine role for User""" - ADMIN = "ADMIN" - EMPLOYEE = "EMPLOYEE" - VOLUNTEER = "VOLUNTEER" + ADMIN = 'ADMIN' + EMPLOYEE = 'EMPLOYEE' + VOLUNTEER = 'VOLUNTEER' def __init__(self): super().__init__(name="role_enum") diff --git a/backend/models/enum_for_models.py b/backend/models/enum_for_models.py index 8e6cdfe..409e37d 100644 --- a/backend/models/enum_for_models.py +++ b/backend/models/enum_for_models.py @@ -6,12 +6,12 @@ from typing import Optional class ProgramTypeEnum(str, Enum): - DOMESTIC = "DOMESTIC" - ECONOMIC = "ECONOMIC" - COMMUNITY = "COMMUNITY" + DOMESTIC = 'DOMESTIC' + ECONOMIC = 'ECONOMIC' + COMMUNITY = 'COMMUNITY' class UserTypeEnum(str, Enum): - ADMIN = "ADMIN" - EMPLOYEE = "EMPLOYEE" - VOLUNTEER = "VOLUNTEER" + ADMIN = 'ADMIN' + EMPLOYEE = 'EMPLOYEE' + VOLUNTEER = 'VOLUNTEER' diff --git a/backend/models/user_model.py b/backend/models/user_model.py index c881d54..2f227c2 100644 --- a/backend/models/user_model.py +++ b/backend/models/user_model.py @@ -12,6 +12,6 @@ class User(BaseModel): email: str = Field(..., description="The e-mail of the user") experience: int = Field(..., description="Years of Experience of the User") group: str - programtype: List[ProgramTypeEnum] - usertype: UserTypeEnum + programtype: List[str] + usertype: str created_at: Optional[datetime] diff --git a/backend/services/__init__.py b/backend/services/__init__.py index e69de29..4067973 100644 --- a/backend/services/__init__.py +++ b/backend/services/__init__.py @@ -0,0 +1,4 @@ +from .user import UserService +from .resource import ResourceService +from .tag import TagService +from .service import ServiceService \ No newline at end of file diff --git a/backend/services/resouce.py b/backend/services/resource.py similarity index 100% rename from backend/services/resouce.py rename to backend/services/resource.py diff --git a/backend/services/tag.py b/backend/services/tag.py index b83fe63..dfc369a 100644 --- a/backend/services/tag.py +++ b/backend/services/tag.py @@ -1,6 +1,9 @@ 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: @@ -8,6 +11,10 @@ class TagService: def __init__(self, session: Session = Depends(db_session)): self._session = session -#get all tags - emma - def get_all_tags(): - return \ No newline at end of file + 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] diff --git a/backend/services/user.py b/backend/services/user.py index abbe8e4..87baab5 100644 --- a/backend/services/user.py +++ b/backend/services/user.py @@ -12,27 +12,24 @@ class UserService: self._session = session - def get_user_by_id(self) -> User: + def get_user_by_id(self, id: int) -> User: """ Gets a user by id from the database Returns: A User Pydantic model """ - user = ( - self._session.query(UserEntity) - .filter(UserEntity.id == id) - ) + query = select(UserEntity).where(UserEntity.id == id) + user_entity: UserEntity | None = self._session.scalar(query) - if user is None: + if user_entity is None: raise Exception( f"No user found with matching id: {id}" ) - return user.to_model() + return user_entity.to_model() -#get users def all(self) -> list[User]: """ Returns a list of all Users @@ -44,7 +41,6 @@ class UserService: return [entity.to_model() for entity in entities] -#post user def create(self, user: User) -> User: """ @@ -55,18 +51,16 @@ class UserService: Returns: User model """ + try: + user_entity = self.get_user_by_id(user.id) + except: + # if does not exist, create new object + user_entity = UserEntity.from_model(user) - #handle if id exists - if user.id: - user.id = None - - # if does not exist, create new object - user_entity = UserEntity.from_model(user) - - # add new user to table - self._session.add(user_entity) - self._session.commit() - - # return added object - return user_entity.to_model() + # add new user to table + self._session.add(user_entity) + self._session.commit() + finally: + # return added object + return user_entity.to_model() diff --git a/backend/test/entities/conftest.py b/backend/test/conftest.py similarity index 78% rename from backend/test/entities/conftest.py rename to backend/test/conftest.py index 63e15a5..679a00b 100644 --- a/backend/test/entities/conftest.py +++ b/backend/test/conftest.py @@ -4,10 +4,11 @@ import pytest from sqlalchemy import Engine, create_engine, text from sqlalchemy.orm import Session from sqlalchemy.exc import OperationalError +from .services import user_test_data, tag_test_data -from ...database import _engine_str -from ...env import getenv -from ... import entities +from ..database import _engine_str +from ..env import getenv +from .. import entities POSTGRES_DATABASE = f'{getenv("POSTGRES_DATABASE")}_test' POSTGRES_USER = getenv("POSTGRES_USER") @@ -48,3 +49,10 @@ def session(test_engine: Engine): yield session finally: session.close() + +@pytest.fixture(autouse=True) +def setup_insert_data_fixture(session: Session): + user_test_data.insert_fake_data(session) + tag_test_data.insert_fake_data(session) + session.commit() + yield diff --git a/backend/test/entities/tag_entity_test.py b/backend/test/entities/tag_entity_test.py index b2b2f7f..7f58b58 100644 --- a/backend/test/entities/tag_entity_test.py +++ b/backend/test/entities/tag_entity_test.py @@ -1,19 +1,4 @@ -""" Testing Tag Entity """ -from sqlalchemy import Engine -from ... import entities -from ...entities.tag_entity import TagEntity - - -def test_add_sample_data_tag(session: Engine): - - """Inserts a sample data point and verifies it is in the database""" - entity = TagEntity(content="Test tag") - session.add(entity) - session.commit() - data = session.get(TagEntity, 1) - assert data.id == 1 - assert data.content == "Test tag" \ No newline at end of file diff --git a/backend/test/entities/user_entity_test.py b/backend/test/entities/user_entity_test.py index 1f775ce..e69de29 100644 --- a/backend/test/entities/user_entity_test.py +++ b/backend/test/entities/user_entity_test.py @@ -1,24 +0,0 @@ -""" Testing User Entity """ - -from sqlalchemy import Engine -from ... import entities -from ...entities.user_entity import UserEntity -from ...entities.user_entity import RoleEnum -from ...entities.user_entity import ProgramEnum - -def test_add_sample_data_user(session: Engine): - - - """Inserts a sample data point and verifies it is in the database""" - entity = UserEntity(id=1, username="emmalynf", role=RoleEnum.ADMIN, email="efoster@unc.edu", program=[ProgramEnum.COMMUNITY, ProgramEnum.DOMESTIC, ProgramEnum.ECONOMIC], experience=10, group="group") - session.add(entity) - session.commit() - data = session.get(UserEntity, 1) - assert data.id == 1 - assert data.username == "emmalynf" - assert data.email == "efoster@unc.edu" - assert data.experience == 10 - assert data.role == RoleEnum.ADMIN - assert data.program == [ProgramEnum.COMMUNITY, ProgramEnum.DOMESTIC, ProgramEnum.ECONOMIC] - - \ No newline at end of file diff --git a/backend/test/services/__init__.py b/backend/test/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/test/services/fixtures.py b/backend/test/services/fixtures.py new file mode 100644 index 0000000..e6fd67b --- /dev/null +++ b/backend/test/services/fixtures.py @@ -0,0 +1,20 @@ +"""Fixtures used for testing the core services.""" + +import pytest +from unittest.mock import create_autospec +from sqlalchemy.orm import Session +from ...services import UserService +from ...services import TagService + + + + +@pytest.fixture() +def user_svc(session: Session): + """This fixture is used to test the UserService class""" + return UserService(session) + +@pytest.fixture() +def tag_svc(session: Session): + """This fixture is used to test the TagService class""" + return TagService(session) diff --git a/backend/test/services/tag_test.py b/backend/test/services/tag_test.py index e69de29..fe7597c 100644 --- a/backend/test/services/tag_test.py +++ b/backend/test/services/tag_test.py @@ -0,0 +1,14 @@ +"""Tests for the TagService class.""" + +# PyTest +import pytest +from ...services.tag import TagService +from .fixtures import tag_svc +from .tag_test_data import tag1, tag2, tag3 +from . import tag_test_data + + +def test_get_all(tag_svc: TagService): + """Test that all tags can be retrieved.""" + tags = tag_svc.all() + assert len(tags) == 3 \ No newline at end of file diff --git a/backend/test/services/tag_test_data.py b/backend/test/services/tag_test_data.py new file mode 100644 index 0000000..cb16e5c --- /dev/null +++ b/backend/test/services/tag_test_data.py @@ -0,0 +1,72 @@ +import pytest +from sqlalchemy.orm import Session +from ...models.tag_model import Tag + +from ...entities.tag_entity import TagEntity +from datetime import datetime + +tag1 = Tag(id=1, content="Tag 1", created_at=datetime.now()) + +tag2 = Tag(id=2, content="Tag 2", created_at=datetime.now()) + +tag3 = Tag(id=3, content="Tag 3", created_at=datetime.now()) + +tagToCreate = Tag(id=4, content="Tag 4", created_at=datetime.now()) + +tags = [tag1, tag2, tag3] + + +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 tags + + # Create entities for test organization data + entities = [] + for tag in tags: + entity = TagEntity.from_model(tag) + session.add(entity) + entities.append(entity) + + # Reset table IDs to prevent ID conflicts + reset_table_id_seq(session, TagEntity, TagEntity.id, len(tags) + 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 diff --git a/backend/test/services/user_test.py b/backend/test/services/user_test.py index b17d98e..337ee5f 100644 --- a/backend/test/services/user_test.py +++ b/backend/test/services/user_test.py @@ -3,53 +3,45 @@ # PyTest import pytest -from ...models.user_model import User from ...services import UserService +from .fixtures import user_svc +from ...models.enum_for_models import ProgramTypeEnum -from ...models.user_model import User -from ...entities.program_enum import ProgramEnum -from ...entities.user_enum import RoleEnum -from ...entities.user_entity import UserEntity +from .user_test_data import employee, volunteer, admin, newUser +from . import user_test_data -programs = ProgramEnum -roles = RoleEnum +def test_create(user_svc: UserService): + """Test creating a user""" + user1 = user_svc.create(newUser) + assert user1 is not None + assert user1.id is not None -volunteer = User( - id = 1, - username="volunteer", - email="volunteer@compass.com", - experience="1 year", - group="volunteers", - programtype=[programs.ECONOMIC], - usertype=roles.VOLUNTEER, -) - -employee = User( - id = 2, - username="employee", - email="employee@compass.com", - experience="5 years", - group="employees", - programtype=[programs.DOMESTIC, programs.COMMUNITY], - usertype=roles.EMPLOYEE, -) - -admin = User( - id = 3, - username="admin", - email="admin@compass.com", - experience="10 years", - group="admin", - programtype=[programs.DOMESTIC, programs.COMMUNITY, programs.ECONOMIC], - usertype=roles.ADMIN, -) - -users=[volunteer, employee, admin] +def test_create_id_exists(user_svc: UserService): + """Test creating a user with id conflict""" + user1 = user_svc.create(volunteer) + assert user1 is not None + assert user1.id is not None -def test_get_all(): +def test_get_all(user_svc: UserService): """Test that all users can be retrieved.""" + users = user_svc.all() + assert len(users) == 3 + +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) + assert user is not None + assert user.id is not None + +def test_get_user_by_id_nonexistent(user_svc: UserService): + """Test getting a user by id that does not exist""" + with pytest.raises(Exception): + user_svc.get_by_id(5) + + + \ No newline at end of file diff --git a/backend/test/services/user_test_data.py b/backend/test/services/user_test_data.py index 9cbb2ed..119375e 100644 --- a/backend/test/services/user_test_data.py +++ b/backend/test/services/user_test_data.py @@ -1,47 +1,61 @@ - import pytest from sqlalchemy.orm import Session from ...models.user_model import User -from ...entities.program_enum import ProgramEnum -from ...entities.user_enum import RoleEnum + +# import model enums instead +from ...models.enum_for_models import UserTypeEnum, ProgramTypeEnum from ...entities.user_entity import UserEntity +from datetime import datetime -programs = ProgramEnum -roles = RoleEnum +programs = ProgramTypeEnum +roles = UserTypeEnum volunteer = User( - id = 1, + id=1, username="volunteer", email="volunteer@compass.com", - experience="1 year", + experience=1, group="volunteers", - programtype=[programs.ECONOMIC], - usertype=roles.VOLUNTEER, + programtype=[programs.COMMUNITY.value], + created_at=datetime.now(), + usertype=UserTypeEnum.VOLUNTEER.value ) employee = User( - id = 2, + id=2, username="employee", email="employee@compass.com", - experience="5 years", + experience=5, group="employees", - programtype=[programs.DOMESTIC, programs.COMMUNITY], - usertype=roles.EMPLOYEE, + programtype=[programs.DOMESTIC.value, programs.ECONOMIC.value], + created_at=datetime.now(), + usertype=roles.EMPLOYEE.value, ) admin = User( - id = 3, + id=3, username="admin", email="admin@compass.com", - experience="10 years", + experience=10, group="admin", - programtype=[programs.DOMESTIC, programs.COMMUNITY, programs.ECONOMIC], - usertype=roles.ADMIN, + programtype=[programs.ECONOMIC.value, programs.DOMESTIC.value, programs.COMMUNITY.value], + created_at=datetime.now(), + usertype=roles.ADMIN.value, ) -users=[volunteer, employee, admin] +newUser = User( + id=4, + username="new", + email="new@compass.com", + experience=1, + group="volunteer", + programtype=[programs.ECONOMIC.value], + created_at=datetime.now(), + usertype=roles.VOLUNTEER.value +) +users = [volunteer, employee, admin] from sqlalchemy import text @@ -83,9 +97,7 @@ def insert_fake_data(session: Session): entities.append(entity) # Reset table IDs to prevent ID conflicts - reset_table_id_seq( - session, UserEntity, UserEntity.id, len(users) + 1 - ) + reset_table_id_seq(session, UserEntity, UserEntity.id, len(users) + 1) # Commit all changes session.commit() @@ -99,4 +111,4 @@ def fake_data_fixture(session: Session): """ insert_fake_data(session) session.commit() - yield \ No newline at end of file + yield