Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions api/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
# ===============================================================================

from fastapi import Depends, APIRouter, Query
from starlette import status
from starlette.status import HTTP_201_CREATED, HTTP_204_NO_CONTENT

from api.pagination import CustomPage
from core.dependencies import (
Expand All @@ -25,10 +25,9 @@
viewer_function,
)
from db import adder
from db.group import Group, GroupThingAssociation
from db.group import Group
from schemas.group import UpdateGroup, CreateGroup, GroupResponse
from schemas.location import CreateGroupThing
from services.crud_helper import model_patcher
from services.crud_helper import model_patcher, model_deleter
from services.query_helper import (
simple_get_by_id,
paginated_all_getter,
Expand All @@ -38,8 +37,10 @@
prefix="/group", tags=["group"], dependencies=[Depends(viewer_function)]
)

# POST =========================================================================

@router.post("", summary="Create a new group", status_code=status.HTTP_201_CREATED)

@router.post("", summary="Create a new group", status_code=HTTP_201_CREATED)
def create_group(
group_data: CreateGroup, session: session_dependency, user: admin_dependency
) -> GroupResponse:
Expand Down Expand Up @@ -110,4 +111,14 @@ async def update_group(
return model_patcher(session, Group, group_id, group_data, user=user)


# DELETE =======================================================================
@router.delete(
"/{group_id}", summary="Delete a group by ID", status_code=HTTP_204_NO_CONTENT
)
async def delete_group(
user: admin_dependency, group_id: int, session: session_dependency
):
return model_deleter(session, Group, group_id)


# ============= EOF =============================================
25 changes: 15 additions & 10 deletions api/location.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
# ===============================================================================
from fastapi import Depends, Query, Response
from fastapi import Query, Response
from fastapi_pagination.ext.sqlalchemy import paginate
from sqlalchemy import select, func
from sqlalchemy.orm import Session
from starlette import status
from api.pagination import CustomPage
from constants import SRID_WGS84
from core.dependencies import session_dependency
from core.dependencies import (
session_dependency,
admin_dependency,
editor_dependency,
viewer_dependency,
)
from db import adder
from db.location import Location
from db.engine import get_db_session
from schemas.location import CreateLocation, LocationResponse, UpdateLocation
from services.geospatial_helper import make_within_wkt
from services.query_helper import make_query, order_sort_filter, simple_get_by_id
Expand All @@ -41,12 +44,12 @@
status_code=status.HTTP_201_CREATED,
)
def create_location(
location_data: CreateLocation, session: Session = Depends(get_db_session)
location_data: CreateLocation, session: session_dependency, user: admin_dependency
) -> LocationResponse:
"""
Create a new sample location in the database.
"""
return adder(session, Location, location_data)
return adder(session, Location, location_data, user=user)


@router.patch(
Expand All @@ -56,12 +59,13 @@ def create_location(
def update_location(
location_id: int,
location_data: UpdateLocation,
session: Session = Depends(get_db_session),
session: session_dependency,
user: editor_dependency,
) -> LocationResponse:
"""
Update a sample location in the database.
"""
return model_patcher(session, Location, location_id, location_data)
return model_patcher(session, Location, location_id, location_data, user=user)


# @router.get("/shapefile", summary="Get location as shapefile")
Expand Down Expand Up @@ -125,6 +129,7 @@ def update_location(
)
async def get_location(
session: session_dependency,
user: viewer_dependency,
nearby_point: str = None,
nearby_distance_km: float = 1,
within: str = None,
Expand Down Expand Up @@ -160,7 +165,7 @@ async def get_location(
summary="Get location by ID",
)
async def get_location_by_id(
location_id: int, session: Session = Depends(get_db_session)
location_id: int, session: session_dependency, user: viewer_dependency
) -> LocationResponse:
"""
Retrieve a sample location by ID from the database.
Expand All @@ -171,7 +176,7 @@ async def get_location_by_id(

@router.delete("/{location_id}", summary="Delete location by ID")
async def delete_location(
location_id: int, session: Session = Depends(get_db_session)
location_id: int, session: session_dependency, user: admin_dependency
) -> Response:
"""
Delete a sample location by ID from the database.
Expand Down
27 changes: 17 additions & 10 deletions api/sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,18 @@
# limitations under the License.
# ===============================================================================

from fastapi import APIRouter, Depends, Query, Response
from fastapi import APIRouter, Query, Response
from sqlalchemy.exc import IntegrityError, ProgrammingError
from sqlalchemy.orm import Session
from starlette.status import HTTP_201_CREATED, HTTP_409_CONFLICT

from api.pagination import CustomPage
from core.dependencies import session_dependency
from core.dependencies import (
session_dependency,
admin_dependency,
editor_dependency,
viewer_dependency,
)
from db import adder
from db.engine import get_db_session
from db.sample import Sample
from schemas import ResourceNotFoundResponse
from schemas.sample import SampleResponse, CreateSample, UpdateSample
Expand Down Expand Up @@ -70,13 +73,13 @@ def database_error_handler(
# ============= Post =============================================
@router.post("", status_code=HTTP_201_CREATED)
def add_sample(
sample_data: CreateSample, session: session_dependency
sample_data: CreateSample, session: session_dependency, user: admin_dependency
) -> SampleResponse:
"""
Endpoint to add a sample.
"""
try:
return adder(session, Sample, sample_data)
return adder(session, Sample, sample_data, user=user)
except (IntegrityError, ProgrammingError) as e:
database_error_handler(sample_data, e)

Expand All @@ -86,7 +89,8 @@ def add_sample(
def update_sample(
sample_id: int,
sample_data: UpdateSample,
session: Session = Depends(get_db_session),
session: session_dependency,
user: editor_dependency,
) -> SampleResponse | ResourceNotFoundResponse:
"""
Endpoint to update a sample.
Expand All @@ -105,7 +109,7 @@ def update_sample(
the update.
"""
try:
return model_patcher(session, Sample, sample_id, sample_data)
return model_patcher(session, Sample, sample_id, sample_data, user=user)
except (IntegrityError, ProgrammingError) as e:
database_error_handler(sample_data, e)

Expand All @@ -114,6 +118,7 @@ def update_sample(
@router.get("", summary="Get Samples")
def get_samples(
session: session_dependency,
user: viewer_dependency,
sort: str = None,
order: str = None,
filter_: str = Query(alias="filter", default=None),
Expand All @@ -129,7 +134,7 @@ def get_samples(

@router.get("/{sample_id}", summary="Get Sample by ID")
def get_sample_by_id(
sample_id: int, session: session_dependency
sample_id: int, session: session_dependency, user: viewer_dependency
) -> SampleResponse | ResourceNotFoundResponse:
"""
Endpoint to retrieve a sample by its ID.
Expand All @@ -141,7 +146,9 @@ def get_sample_by_id(


@router.delete("/{sample_id}", summary="Delete Sample by ID")
def delete_sample_by_id(sample_id: int, session: session_dependency) -> Response:
def delete_sample_by_id(
sample_id: int, session: session_dependency, user: admin_dependency
) -> Response:
return model_deleter(session, Sample, sample_id)


Expand Down
27 changes: 20 additions & 7 deletions api/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@
from starlette import status

from api.pagination import CustomPage
from core.dependencies import session_dependency
from core.dependencies import (
session_dependency,
admin_dependency,
editor_dependency,
viewer_dependency,
)
from db import adder, Observation, Sample
from db.sensor import Sensor
from schemas.sensor import SensorResponse, CreateSensor, UpdateSensor
Expand All @@ -35,20 +40,23 @@

@router.post("", status_code=status.HTTP_201_CREATED)
def add_sensor(
sensor_data: CreateSensor, session: session_dependency
sensor_data: CreateSensor, session: session_dependency, user: admin_dependency
) -> SensorResponse:
"""
Add a sensor to the system.
"""
return adder(session, Sensor, sensor_data)
return adder(session, Sensor, sensor_data, user=user)


# ====== PATCH =================================================================


@router.patch("/{sensor_id}", status_code=status.HTTP_200_OK)
def update_sensor(
sensor_id: int, sensor_data: UpdateSensor, session: session_dependency
sensor_id: int,
sensor_data: UpdateSensor,
session: session_dependency,
user: editor_dependency,
) -> SensorResponse:
"""
Update a sensor in the system.
Expand Down Expand Up @@ -97,14 +105,16 @@ def update_sensor(
status_code=status.HTTP_409_CONFLICT, detail=[detail]
)

return model_patcher(session, Sensor, sensor_id, sensor_data)
return model_patcher(session, Sensor, sensor_id, sensor_data, user=user)


# ====== DELETE ================================================================


@router.delete("/{sensor_id}")
def delete_sensor(sensor_id: int, session: session_dependency) -> Response:
def delete_sensor(
sensor_id: int, session: session_dependency, user: admin_dependency
) -> Response:
"""
Delete a sensor in the system
"""
Expand All @@ -117,6 +127,7 @@ def delete_sensor(sensor_id: int, session: session_dependency) -> Response:
@router.get("", status_code=status.HTTP_200_OK)
def get_sensors(
session: session_dependency,
user: viewer_dependency,
thing_id: int = None, # Optional filter for thing_id
observed_property: str = None, # Optional filter for observed_property
sort: str | None = None,
Expand Down Expand Up @@ -151,7 +162,9 @@ def get_sensors(


@router.get("/{sensor_id}", status_code=status.HTTP_200_OK)
def get_sensor(sensor_id: int, session: session_dependency) -> SensorResponse:
def get_sensor(
sensor_id: int, session: session_dependency, user: viewer_dependency
) -> SensorResponse:
"""
Retrieve a sensor by its ID.
"""
Expand Down
38 changes: 16 additions & 22 deletions schemas/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,25 @@
# ===============================================================================
from geoalchemy2 import WKBElement
from geoalchemy2.shape import to_shape
from pydantic import BaseModel, field_validator
from pydantic import BaseModel, field_validator, model_validator
from typing_extensions import Self

from schemas import ORMBaseModel
from services.validation.geospatial import validate_wkt_geometry


class ValidateGroup(BaseModel):
project_area: str | None = None

description: str | None = None
parent_group_id: int | None = None

@classmethod
@field_validator("project_area")
def validate_area_is_wkt(cls, wkt):
return validate_wkt_geometry(wkt)
valid_wkt = validate_wkt_geometry(wkt)
if "MULTIPOLYGON" not in valid_wkt:
raise ValueError("WKT must be a valid MULTIPOLYGON")

return valid_wkt


# -------- CREATE ----------
Expand All @@ -49,25 +52,16 @@ class GroupResponse(ORMBaseModel):
This model can be extended to include additional fields as needed.
"""

id: int
name: str
description: str | None = None
parent_group_id: int | None = None

@classmethod
@field_validator("project_area", mode="before")
def project_area_to_wkt(cls, value):
if not value:
return value

if isinstance(value, WKBElement):
return to_shape(value).wkt

# If the value is a string, assume it's already in WKT format
if isinstance(value, str):
return value

return None
project_area: str | None
description: str | None
parent_group_id: int | None

@model_validator(mode="before")
def project_area_to_wkt(self: Self) -> Self:
if isinstance(self.project_area, WKBElement):
self.project_area = to_shape(self.project_area).wkt
return self


# -------- UPDATE ----------
Expand Down
Loading
Loading