mirror of
https://github.com/cssgunc/compass.git
synced 2025-04-09 14:00:15 -04:00
Implement resource api methods
This commit is contained in:
parent
4e090e5bd5
commit
ba15bf7519
26
backend/api/resource.py
Normal file
26
backend/api/resource.py
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
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)
|
|
@ -15,6 +15,7 @@ from datetime import datetime
|
||||||
# Import self for to model
|
# Import self for to model
|
||||||
from typing import Self
|
from typing import Self
|
||||||
from backend.entities.program_enum import Program_Enum
|
from backend.entities.program_enum import Program_Enum
|
||||||
|
from ..models.resource_model import Resource
|
||||||
|
|
||||||
|
|
||||||
class ResourceEntity(EntityBase):
|
class ResourceEntity(EntityBase):
|
||||||
|
@ -34,34 +35,33 @@ class ResourceEntity(EntityBase):
|
||||||
back_populates="resource", cascade="all,delete"
|
back_populates="resource", cascade="all,delete"
|
||||||
)
|
)
|
||||||
|
|
||||||
#
|
@classmethod
|
||||||
# @classmethod
|
def from_model(cls, model: Resource) -> Self:
|
||||||
# def from_model(cls, model: user_model) -> Self:
|
"""
|
||||||
# """
|
Create a UserEntity from a User model.
|
||||||
# Create a UserEntity from a User model.
|
|
||||||
|
|
||||||
# Args:
|
Args:
|
||||||
# model (User): The model to create the entity from.
|
model (User): The model to create the entity from.
|
||||||
|
|
||||||
# Returns:
|
Returns:
|
||||||
# Self: The entity (not yet persisted).
|
Self: The entity (not yet persisted).
|
||||||
# """
|
"""
|
||||||
|
|
||||||
# return cls (
|
return cls(
|
||||||
# id = model.id,
|
id=model.id,
|
||||||
# created_at = model.created_at,
|
created_at=model.created_at,
|
||||||
# name = model.name,
|
name=model.name,
|
||||||
# summary = model.summary,
|
summary=model.summary,
|
||||||
# link = model.link,
|
link=model.link,
|
||||||
# program = model.program,
|
program=model.program,
|
||||||
# )
|
)
|
||||||
|
|
||||||
# def to_model(self) -> user_model:
|
def to_model(self) -> Resource:
|
||||||
# return user_model (
|
return Resource(
|
||||||
# id = self.id,
|
id=self.id,
|
||||||
# created_at = self.created_at,
|
created_at=self.created_at,
|
||||||
# name = self.name,
|
name=self.name,
|
||||||
# summary = self.summary,
|
summary=self.summary,
|
||||||
# link = self.link,
|
link=self.link,
|
||||||
# program = self.program,
|
program=self.program,
|
||||||
# )
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ from fastapi import FastAPI, Request
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
from fastapi.middleware.gzip import GZipMiddleware
|
||||||
|
|
||||||
from .api import user, health, service
|
from .api import user, health, service, resource
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
Welcome to the **COMPASS** RESTful Application Programming Interface.
|
||||||
|
@ -12,12 +12,17 @@ app = FastAPI(
|
||||||
title="Compass API",
|
title="Compass API",
|
||||||
version="0.0.1",
|
version="0.0.1",
|
||||||
description=description,
|
description=description,
|
||||||
openapi_tags=[user.openapi_tags, health.openapi_tags, service.openapi_tags],
|
openapi_tags=[
|
||||||
|
user.openapi_tags,
|
||||||
|
health.openapi_tags,
|
||||||
|
service.openapi_tags,
|
||||||
|
resource.openapi_tags,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
app.add_middleware(GZipMiddleware)
|
app.add_middleware(GZipMiddleware)
|
||||||
|
|
||||||
feature_apis = [user, health, service]
|
feature_apis = [user, health, service, resource]
|
||||||
|
|
||||||
for feature_api in feature_apis:
|
for feature_api in feature_apis:
|
||||||
app.include_router(feature_api.api)
|
app.include_router(feature_api.api)
|
||||||
|
|
|
@ -4,7 +4,6 @@ from typing import List
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .enum_for_models import ProgramTypeEnum
|
from .enum_for_models import ProgramTypeEnum
|
||||||
from .resource_model import Resource
|
|
||||||
|
|
||||||
|
|
||||||
class Resource(BaseModel):
|
class Resource(BaseModel):
|
||||||
|
@ -12,5 +11,5 @@ class Resource(BaseModel):
|
||||||
name: str = Field(..., max_length=150, description="The name of the resource")
|
name: str = Field(..., max_length=150, description="The name of the resource")
|
||||||
summary: str = Field(..., max_length=300, description="The summary of the resource")
|
summary: str = Field(..., max_length=300, description="The summary of the resource")
|
||||||
link: str = Field(..., max_length=150, description="link to the resource")
|
link: str = Field(..., max_length=150, description="link to the resource")
|
||||||
programtype: ProgramTypeEnum
|
program: ProgramTypeEnum
|
||||||
created_at: Optional[datetime]
|
created_at: Optional[datetime]
|
||||||
|
|
|
@ -6,7 +6,7 @@ from ..database import engine, _engine_str
|
||||||
from ..env import getenv
|
from ..env import getenv
|
||||||
from .. import entities
|
from .. import entities
|
||||||
|
|
||||||
from ..test.services import user_test_data, service_test_data
|
from ..test.services import user_test_data, service_test_data, resource_test_data
|
||||||
|
|
||||||
database = getenv("POSTGRES_DATABASE")
|
database = getenv("POSTGRES_DATABASE")
|
||||||
|
|
||||||
|
@ -23,5 +23,5 @@ entities.EntityBase.metadata.create_all(engine)
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
user_test_data.insert_test_data(session)
|
user_test_data.insert_test_data(session)
|
||||||
service_test_data.insert_fake_data(session)
|
service_test_data.insert_fake_data(session)
|
||||||
|
resource_test_data.insert_fake_data(session)
|
||||||
session.commit()
|
session.commit()
|
||||||
|
|
|
@ -4,33 +4,33 @@ from sqlalchemy.orm import Session
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from ..models.resource_model import Resource
|
from ..models.resource_model import Resource
|
||||||
from ..entities.resource_entity import ResourceEntity
|
from ..entities.resource_entity import ResourceEntity
|
||||||
from ..models.user_model import User
|
from ..models.user_model import User, UserTypeEnum
|
||||||
|
|
||||||
from .exceptions import ResourceNotFoundException
|
from .exceptions import ResourceNotFoundException
|
||||||
|
|
||||||
|
|
||||||
class ResourceService:
|
class ResourceService:
|
||||||
|
|
||||||
def __init__(self, session: Session = Depends(db_session)):
|
def __init__(self, session: Session = Depends(db_session)):
|
||||||
self._session = session
|
self._session = session
|
||||||
|
|
||||||
def all(self, user: User) -> list[Resource]:
|
def get_resource_by_user(self, subject: User):
|
||||||
"""
|
"""Resource method getting all of the resources that a user has access to based on role"""
|
||||||
Retrieves all Resources that the user has access to based on their role and group.
|
if subject.role != UserTypeEnum.VOLUNTEER:
|
||||||
|
query = select(ResourceEntity)
|
||||||
|
entities = self._session.scalars(query).all()
|
||||||
|
|
||||||
Parameters:
|
return [resource.to_model() for resource in entities]
|
||||||
user: a valid User model representing the currently logged in User
|
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)
|
||||||
|
|
||||||
Returns:
|
return [resource.to_model() for resource in resources]
|
||||||
list[Resource]: list of accessible `Resource` for the user
|
|
||||||
"""
|
|
||||||
# Filter resources based on user's role and group
|
|
||||||
query = select(ResourceEntity).where(
|
|
||||||
ResourceEntity.role == user.role,
|
|
||||||
ResourceEntity.group == user.group
|
|
||||||
)
|
|
||||||
entities = self._session.scalars(query).all()
|
|
||||||
|
|
||||||
return [entity.to_model() for entity in entities]
|
|
||||||
|
|
||||||
def create(self, user: User, resource: Resource) -> Resource:
|
def create(self, user: User, resource: Resource) -> Resource:
|
||||||
"""
|
"""
|
||||||
|
@ -44,7 +44,9 @@ class ResourceService:
|
||||||
Resource: Object added to table
|
Resource: Object added to table
|
||||||
"""
|
"""
|
||||||
if resource.role != user.role or resource.group != user.group:
|
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.")
|
raise PermissionError(
|
||||||
|
"User does not have permission to add resources in this role or group."
|
||||||
|
)
|
||||||
|
|
||||||
resource_entity = ResourceEntity.from_model(resource)
|
resource_entity = ResourceEntity.from_model(resource)
|
||||||
self._session.add(resource_entity)
|
self._session.add(resource_entity)
|
||||||
|
@ -68,7 +70,11 @@ class ResourceService:
|
||||||
"""
|
"""
|
||||||
resource = (
|
resource = (
|
||||||
self._session.query(ResourceEntity)
|
self._session.query(ResourceEntity)
|
||||||
.filter(ResourceEntity.id == id, ResourceEntity.role == user.role, ResourceEntity.group == user.group)
|
.filter(
|
||||||
|
ResourceEntity.id == id,
|
||||||
|
ResourceEntity.role == user.role,
|
||||||
|
ResourceEntity.group == user.group,
|
||||||
|
)
|
||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -92,12 +98,16 @@ class ResourceService:
|
||||||
ResourceNotFoundException: If no resource is found with the corresponding ID
|
ResourceNotFoundException: If no resource is found with the corresponding ID
|
||||||
"""
|
"""
|
||||||
if resource.role != user.role or resource.group != user.group:
|
if resource.role != user.role or resource.group != user.group:
|
||||||
raise PermissionError("User does not have permission to update this resource.")
|
raise PermissionError(
|
||||||
|
"User does not have permission to update this resource."
|
||||||
|
)
|
||||||
|
|
||||||
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
|
obj = self._session.get(ResourceEntity, resource.id) if resource.id else None
|
||||||
|
|
||||||
if obj is None:
|
if obj is None:
|
||||||
raise ResourceNotFoundException(f"No resource found with matching id: {resource.id}")
|
raise ResourceNotFoundException(
|
||||||
|
f"No resource found with matching id: {resource.id}"
|
||||||
|
)
|
||||||
|
|
||||||
obj.update_from_model(resource) # Assuming an update method exists
|
obj.update_from_model(resource) # Assuming an update method exists
|
||||||
self._session.commit()
|
self._session.commit()
|
||||||
|
@ -117,7 +127,11 @@ class ResourceService:
|
||||||
"""
|
"""
|
||||||
resource = (
|
resource = (
|
||||||
self._session.query(ResourceEntity)
|
self._session.query(ResourceEntity)
|
||||||
.filter(ResourceEntity.id == id, ResourceEntity.role == user.role, ResourceEntity.group == user.group)
|
.filter(
|
||||||
|
ResourceEntity.id == id,
|
||||||
|
ResourceEntity.role == user.role,
|
||||||
|
ResourceEntity.group == user.group,
|
||||||
|
)
|
||||||
.one_or_none()
|
.one_or_none()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -144,7 +158,7 @@ class ResourceService:
|
||||||
query = select(ResourceEntity).where(
|
query = select(ResourceEntity).where(
|
||||||
ResourceEntity.title.ilike(f"%{search_string}%"),
|
ResourceEntity.title.ilike(f"%{search_string}%"),
|
||||||
ResourceEntity.role == user.role,
|
ResourceEntity.role == user.role,
|
||||||
ResourceEntity.group == user.group
|
ResourceEntity.group == user.group,
|
||||||
)
|
)
|
||||||
entities = self._session.scalars(query).all()
|
entities = self._session.scalars(query).all()
|
||||||
|
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
import pytest
|
|
||||||
from sqlalchemy.orm import Session
|
|
||||||
from sqlalchemy.exc import NoResultFound
|
|
||||||
from .resource_service import ResourceService
|
|
||||||
from .models.resource_model import Resource
|
|
||||||
from .entities.resource_entity import ResourceEntity
|
|
||||||
from .models.user_model import User
|
|
||||||
from .exceptions import ResourceNotFoundException
|
|
||||||
|
|
||||||
# Example of a Resource and User object creation for use in tests
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_resource():
|
|
||||||
return Resource(id=1, name="Sample Resource", summary="A brief summary", link="http://example.com", programtype="TypeA")
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def sample_user():
|
|
||||||
return User(id=1, username="admin", is_admin=True)
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def resource_service(mocker):
|
|
||||||
# Mock the session and its methods
|
|
||||||
mock_session = mocker.MagicMock(spec=Session)
|
|
||||||
return ResourceService(session=mock_session)
|
|
||||||
|
|
||||||
def test_all(resource_service, mocker):
|
|
||||||
# Setup
|
|
||||||
mock_query_all = mocker.MagicMock(return_value=[ResourceEntity(id=1, name="Resource One"), ResourceEntity(id=2, name="Resource Two")])
|
|
||||||
mocker.patch.object(resource_service._session, 'scalars', return_value=mock_query_all)
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
results = resource_service.all()
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
assert len(results) == 2
|
|
||||||
assert results[0].id == 1
|
|
||||||
assert results[1].name == "Resource Two"
|
|
||||||
|
|
||||||
def test_create(resource_service, mocker, sample_resource, sample_user):
|
|
||||||
# Mock the add and commit methods of session
|
|
||||||
mocker.patch.object(resource_service._session, 'add')
|
|
||||||
mocker.patch.object(resource_service._session, 'commit')
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
result = resource_service.create(sample_user, sample_resource)
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
resource_service._session.add.assert_called_once()
|
|
||||||
resource_service._session.commit.assert_called_once()
|
|
||||||
assert result.id == sample_resource.id
|
|
||||||
assert result.name == sample_resource.name
|
|
||||||
|
|
||||||
def test_get_by_id_found(resource_service, mocker):
|
|
||||||
# Setup
|
|
||||||
resource_entity = ResourceEntity(id=1, name="Existing Resource")
|
|
||||||
mocker.patch.object(resource_service._session, 'query', return_value=mocker.MagicMock(one_or_none=mocker.MagicMock(return_value=resource_entity)))
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
result = resource_service.get_by_id(1)
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
assert result.id == 1
|
|
||||||
assert result.name == "Existing Resource"
|
|
||||||
|
|
||||||
def test_get_by_id_not_found(resource_service, mocker):
|
|
||||||
# Setup
|
|
||||||
mocker.patch.object(resource_service._session, 'query', return_value=mocker.MagicMock(one_or_none=mocker.MagicMock(return_value=None)))
|
|
||||||
|
|
||||||
# Execution & Verification
|
|
||||||
with pytest.raises(ResourceNotFoundException):
|
|
||||||
resource_service.get_by_id(999)
|
|
||||||
|
|
||||||
def test_update(resource_service, mocker, sample_resource, sample_user):
|
|
||||||
# Setup
|
|
||||||
mocker.patch.object(resource_service._session, 'get', return_value=sample_resource)
|
|
||||||
mocker.patch.object(resource_service._session, 'commit')
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
result = resource_service.update(sample_user, sample_resource)
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
assert result.id == sample_resource.id
|
|
||||||
resource_service._session.commit.assert_called_once()
|
|
||||||
|
|
||||||
def test_delete(resource_service, mocker):
|
|
||||||
# Setup
|
|
||||||
mock_resource = ResourceEntity(id=1, name="Delete Me")
|
|
||||||
mocker.patch.object(resource_service._session, 'query', return_value=mocker.MagicMock(one_or_none=mocker.MagicMock(return_value=mock_resource)))
|
|
||||||
mocker.patch.object(resource_service._session, 'delete')
|
|
||||||
mocker.patch.object(resource_service._session, 'commit')
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
resource_service.delete(sample_user(), 1)
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
resource_service._session.delete.assert_called_with(mock_resource)
|
|
||||||
resource_service._session.commit.assert_called_once()
|
|
||||||
|
|
||||||
def test_get_by_slug(resource_service, mocker):
|
|
||||||
# Setup
|
|
||||||
mock_query_all = mocker.MagicMock(return_value=[ResourceEntity(id=1, name="Resource One"), ResourceEntity(id=2, name="Resource Two")])
|
|
||||||
mocker.patch.object(resource_service._session, 'scalars', return_value=mock_query_all)
|
|
||||||
|
|
||||||
# Execution
|
|
||||||
results = resource_service.get_by_slug("Resource")
|
|
||||||
|
|
||||||
# Verification
|
|
||||||
|
|
95
backend/test/services/resource_test_data.py
Normal file
95
backend/test/services/resource_test_data.py
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from ...entities import ResourceEntity
|
||||||
|
from ...models.enum_for_models import ProgramTypeEnum
|
||||||
|
from ...models.resource_model import Resource
|
||||||
|
|
||||||
|
resource_1 = Resource(
|
||||||
|
id=1,
|
||||||
|
name="Resource 1",
|
||||||
|
summary="Helpful information for victims of domestic violence",
|
||||||
|
link="https://example.com/resource1",
|
||||||
|
program=ProgramTypeEnum.DOMESTIC,
|
||||||
|
created_at=datetime(2023, 6, 1, 10, 0, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_2 = Resource(
|
||||||
|
id=2,
|
||||||
|
name="Resource 2",
|
||||||
|
summary="Legal assistance resources",
|
||||||
|
link="https://example.com/resource2",
|
||||||
|
program=ProgramTypeEnum.COMMUNITY,
|
||||||
|
created_at=datetime(2023, 6, 2, 12, 30, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_3 = Resource(
|
||||||
|
id=3,
|
||||||
|
name="Resource 3",
|
||||||
|
summary="Financial aid resources",
|
||||||
|
link="https://example.com/resource3",
|
||||||
|
program=ProgramTypeEnum.ECONOMIC,
|
||||||
|
created_at=datetime(2023, 6, 3, 15, 45, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_4 = Resource(
|
||||||
|
id=4,
|
||||||
|
name="Resource 4",
|
||||||
|
summary="Counseling and support groups",
|
||||||
|
link="https://example.com/resource4",
|
||||||
|
program=ProgramTypeEnum.DOMESTIC,
|
||||||
|
created_at=datetime(2023, 6, 4, 9, 15, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
resource_5 = Resource(
|
||||||
|
id=5,
|
||||||
|
name="Resource 5",
|
||||||
|
summary="Shelter and housing resources",
|
||||||
|
link="https://example.com/resource5",
|
||||||
|
program=ProgramTypeEnum.DOMESTIC,
|
||||||
|
created_at=datetime(2023, 6, 5, 11, 30, 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
resources = [resource_1, resource_2, resource_3, resource_4, resource_5]
|
||||||
|
|
||||||
|
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 resource data into the test session."""
|
||||||
|
global resources
|
||||||
|
# Create entities for test resource data
|
||||||
|
entities = []
|
||||||
|
for resource in resources:
|
||||||
|
entity = ResourceEntity.from_model(resource)
|
||||||
|
session.add(entity)
|
||||||
|
entities.append(entity)
|
||||||
|
|
||||||
|
# Reset table IDs to prevent ID conflicts
|
||||||
|
reset_table_id_seq(session, ResourceEntity, ResourceEntity.id, len(resources) + 1)
|
||||||
|
|
||||||
|
# Commit all changes
|
||||||
|
session.commit()
|
Loading…
Reference in New Issue
Block a user