Skip to content
This repository has been archived by the owner on Aug 12, 2024. It is now read-only.

ADA Requests MVP #137

Merged
merged 15 commits into from
Feb 29, 2024
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
37 changes: 37 additions & 0 deletions backend/alembic/versions/38c5326f0334_create_pickup_spots_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""create pickup spots table

Revision ID: 38c5326f0334
Revises: d53db4ccb3fc
Create Date: 2024-02-22 10:17:36.297413

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "38c5326f0334"
down_revision: Union[str, None] = "d53db4ccb3fc"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"pickup_spots",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column("name", sa.String(255), nullable=False),
sa.Column("lat", sa.Float, nullable=False),
sa.Column("lon", sa.Float, nullable=False),
sa.UniqueConstraint("name"),
)


def downgrade() -> None:
op.execute(
"""
DROP TABLE IF EXISTS public.pickup_spots;
"""
)
41 changes: 41 additions & 0 deletions backend/alembic/versions/8166e12f260c_create_ada_requests_table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""create ada requests table

Revision ID: 8166e12f260c
Revises: 38c5326f0334
Create Date: 2024-02-22 10:38:27.424804

"""

from typing import Sequence, Union

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision: str = "8166e12f260c"
down_revision: Union[str, None] = "38c5326f0334"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
op.create_table(
"ada_requests",
sa.Column("id", sa.Integer, primary_key=True),
sa.Column(
"pickup_spot",
sa.Integer,
sa.ForeignKey("pickup_spots.id"),
nullable=False,
),
sa.Column("pickup_time", sa.DateTime(timezone=True), nullable=False),
sa.Column("wheelchair", sa.Boolean, nullable=False),
)


def downgrade() -> None:
op.execute(
"""
DROP TABLE IF EXISTS public.ada_requests;
"""
)
160 changes: 160 additions & 0 deletions backend/src/handlers/ada.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
from datetime import datetime, timezone
from typing import Annotated, Dict, List, Optional, Union

from fastapi import APIRouter, HTTPException, Query, Request
from pydantic import BaseModel
from src.model.ada_request import ADARequest
from src.model.pickup_spot import PickupSpot
from src.request import process_include

router = APIRouter(prefix="/ada", tags=["ada"])

FIELD_PICKUP_SPOTS = "pickup_spot"
INCLUDES = {FIELD_PICKUP_SPOTS}


class PickupSpotModel(BaseModel):
name: str
OxygenCobalt marked this conversation as resolved.
Show resolved Hide resolved
latitude: float
longitude: float


@router.get("/pickup_spots")
def get_pickup_spots(req: Request) -> List[Dict[str, Union[str, int, float]]]:
with req.app.state.db.session() as session:
pickup_spots = session.query(PickupSpot).all()
pickup_spots_json: List[Dict[str, Union[str, int, float]]] = []
for spot in pickup_spots:
spot_json = {
"id": spot.id,
"name": spot.name,
"latitude": spot.lat,
"longitude": spot.lon,
}
pickup_spots_json.append(spot_json)

return pickup_spots_json


@router.post("/pickup_spots")
def post_pickup_spot(spot: PickupSpotModel, req: Request):
new_spot = PickupSpot(name=spot.name, lat=spot.latitude, lon=spot.longitude)

with req.app.state.db.session() as session:
session.add(new_spot)
session.commit()

return {"message": "OK"}


@router.put("/pickup_spots/{id}")
def update_pickup_spot(id: int, spot: PickupSpotModel, req: Request):
with req.app.state.db.session() as session:
pickup_spot = session.query(PickupSpot).filter(PickupSpot.id == id).first()

if pickup_spot is None:
raise HTTPException(status_code=404, detail="Pickup spot not found")

pickup_spot.name = spot.name
OxygenCobalt marked this conversation as resolved.
Show resolved Hide resolved
pickup_spot.lat = spot.latitude
pickup_spot.lon = spot.longitude
session.commit()

return {"message": "OK"}


@router.delete("/pickup_spots/{id}")
def delete_pickup_spot(id: int, req: Request):
with req.app.state.db.session() as session:
pickup_spot = session.query(PickupSpot).filter(PickupSpot.id == id).first()

if pickup_spot is None:
raise HTTPException(status_code=404, detail="Pickup spot not found")

session.query(PickupSpot).filter(PickupSpot.id == id).delete()
session.commit()

return {"message": "OK"}


class ADARequestModel(BaseModel):
pickup_spot_id: int
pickup_time: int
wheelchair: bool


@router.get("/requests")
def get_ada_requests(
req: Request,
filter: Optional[str] = None,
include: Annotated[list[str] | None, Query()] = None,
):
now = datetime.now(timezone.utc)
include_set = process_include(include, INCLUDES)
with req.app.state.db.session() as session:
query = session.query(ADARequest)
if filter == "today":
end_of_day = datetime.now(timezone.utc).replace(
hour=23, minute=59, second=59
)
query = query.filter(
ADARequest.pickup_time >= now, ADARequest.pickup_time <= end_of_day
)
elif filter == "future":
query = query.filter(ADARequest.pickup_time >= now)
elif filter is not None:
raise HTTPException(status_code=400, detail=f"Invalid filter {filter}")
OxygenCobalt marked this conversation as resolved.
Show resolved Hide resolved

ada_requests = query.order_by(ADARequest.pickup_time).all()

result = []
for request in ada_requests:
request_json = {
"id": request.id,
"pickup_time": int(request.pickup_time.timestamp()),
"wheelchair": request.wheelchair,
}
if FIELD_PICKUP_SPOTS in include_set:
spot = (
session.query(PickupSpot)
.filter(PickupSpot.id == request.pickup_spot)
.first()
)
request_json[FIELD_PICKUP_SPOTS] = {
"id": spot.id,
"name": spot.name,
"latitude": spot.lat,
"longitude": spot.lon,
}
result.append(request_json)

return result


@router.post("/requests")
def create_ada_request(
req: Request, ada_request_model: ADARequestModel
) -> dict[str, str]:
pickup_time = datetime.fromtimestamp(ada_request_model.pickup_time, timezone.utc)
# Make sure that the pickup time is in the future
if pickup_time <= datetime.now(timezone.utc):
raise HTTPException(status_code=400, detail="Pickup time must be in the future")

with req.app.state.db.session() as session:
pickup_spot = (
session.query(PickupSpot)
.filter(PickupSpot.id == ada_request_model.pickup_spot_id)
.count()
)
if not pickup_spot:
raise HTTPException(status_code=400, detail="Pickup spot not found")

pickup_spot = ADARequest(
pickup_spot=ada_request_model.pickup_spot_id,
wheelchair=ada_request_model.wheelchair,
pickup_time=pickup_time,
)
session.add(pickup_spot)
session.commit()

return {"message": "OK"}
44 changes: 38 additions & 6 deletions backend/src/handlers/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
from pydantic import BaseModel
from pygeoif.geometry import Point, Polygon
from src.model.alert import Alert
from src.model.pickup_spot import PickupSpot
from src.model.route import Route
from src.model.route_disable import RouteDisable
from src.model.route_stop import RouteStop
Expand Down Expand Up @@ -86,10 +87,9 @@ def get_kml(req: Request):

with req.app.state.db.session() as session:
routes = session.query(Route).all()

stops = session.query(Stop).all()

route_stops = session.query(RouteStop).all()
pickup_spots = session.query(PickupSpot).all()

k = kml.KML()
ns = "{http://www.opengis.net/kml/2.2}"
Expand Down Expand Up @@ -132,10 +132,19 @@ def get_kml(req: Request):
d.append(p)

for stop in stops:
p = kml.Placemark(ns, stop.name, stop.name)
description = "<![CDATA[<div>Stop<br></div>]]>"
p = kml.Placemark(ns, stop.name, stop.name, description=description)
p.geometry = Point(stop.lon, stop.lat)
d.append(p)

for pickup_spot in pickup_spots:
description = "<![CDATA[<div>Pickup Spot<br></div>]]>"
p = kml.Placemark(
ns, pickup_spot.name, pickup_spot.name, description=description
)
p.geometry = Point(pickup_spot.lon, pickup_spot.lat)
d.append(p)

d.append_style(style)

kml_string = k.to_string().replace("&lt;", "<").replace("&gt;", ">")
Expand Down Expand Up @@ -257,6 +266,7 @@ async def create_route(req: Request, kml_file: UploadFile):

routes = {}
stops = {}
pickup_spots = {}

stop_id_map = {}

Expand All @@ -268,7 +278,22 @@ async def create_route(req: Request, kml_file: UploadFile):
if type(feature.geometry) == pygeoif.geometry.Polygon:
routes[feature.name] = feature
elif type(feature.geometry) == pygeoif.geometry.Point:
stops[feature.name] = feature
if feature.description is None:
return HTTPException(status_code=400, detail="bad kml file")

desc_html = BeautifulSoup(
feature.description, features="html.parser"
)

# Want the first div's text contents and then strip all of the tags
typ = desc_html.find("div", recursive=True).text.strip()

if typ == "Stop":
stops[feature.name] = feature
elif typ == "Pickup Spot":
pickup_spots[feature.name] = feature
else:
return HTTPException(status_code=400, detail="invlaid")
else:
return HTTPException(status_code=400, detail="bad kml file")

Expand Down Expand Up @@ -316,8 +341,6 @@ async def create_route(req: Request, kml_file: UploadFile):
for div in route_desc_html.find_all("div", recursive=False)
]

print(route_stops, route_desc_html.find_all("div", recursive=False))

for pos, stop in enumerate(route_stops):
if stop not in stop_id_map:
continue
Expand All @@ -328,6 +351,15 @@ async def create_route(req: Request, kml_file: UploadFile):
session.add(route_stop)
session.flush()

for pickup_spot_name, pickup_spot in pickup_spots.items():
pickup_spot_model = PickupSpot(
name=pickup_spot_name,
lat=pickup_spot.geometry.y,
lon=pickup_spot.geometry.x,
)
session.add(pickup_spot_model)
session.flush()

await kml_file.close()

session.commit()
Expand Down
3 changes: 2 additions & 1 deletion backend/src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from fastapi.middleware.cors import CORSMiddleware

from .db import DBWrapper
from .handlers import alert, ridership, routes, stops, vans
from .handlers import ada, alert, ridership, routes, stops, vans
from .hardware import HardwareExceptionMiddleware
from .vantracking.factory import van_tracker
from .vantracking.tracker import VanTracker
Expand All @@ -21,6 +21,7 @@
)

app.add_middleware(HardwareExceptionMiddleware)
app.include_router(ada.router)
app.include_router(routes.router)
app.include_router(stops.router)
app.include_router(alert.router)
Expand Down
29 changes: 29 additions & 0 deletions backend/src/model/ada_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
from datetime import datetime

from sqlalchemy import ForeignKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column
from src.db import Base
from src.model.types import TZDateTime


class ADARequest(Base):
__tablename__ = "ada_requests"
__table_args__ = (ForeignKeyConstraint(["pickup_spot"], ["pickup_spots.id"]),)
id: Mapped[int] = mapped_column(
primary_key=True, autoincrement=True, nullable=False
)
pickup_spot: Mapped[int] = mapped_column(nullable=False)
pickup_time: Mapped[datetime] = mapped_column(TZDateTime, nullable=False)
wheelchair: Mapped[bool] = mapped_column(nullable=False)

def __eq__(self, __value: object) -> bool:
# Exclude ID since it'll always differ, only compare on content
return (
isinstance(__value, ADARequest)
and self.pickup_spot == __value.pickup_spot
and self.pickup_time == __value.pickup_time
and self.wheelchair == __value.wheelchair
)

def __repr__(self) -> str:
return f"<AdaRequest id={self.id} pickup_spot={self.pickup_spot} pickup_time={self.pickup_time} wheelchair={self.wheelchair}>"
Loading
Loading