Segments phase 1: models, store, and API endpoints (GET/POST/DELETE /api/segments)
This commit is contained in:
@@ -120,6 +120,13 @@ class GenericResponse(BaseModel):
|
||||
ok: bool = Field(True, description="Success flag")
|
||||
|
||||
|
||||
class CreateSegmentRequest(BaseModel):
|
||||
name: str = Field(..., description="Segment name")
|
||||
sport: Optional[str] = Field(default=None, description="Sport filter (e.g. cycling)")
|
||||
polyline: list[list[float]] = Field(..., description="[[lat, lon], ...] ordered GPS points")
|
||||
distance_m: float = Field(..., description="Segment length in metres")
|
||||
|
||||
|
||||
# ── Active job tracker ───────────────────────────────────────────────────────
|
||||
# Tracks in-progress upload/processing jobs so admins can see what's running.
|
||||
# Jobs are added when a streaming upload starts and removed when it finishes.
|
||||
@@ -235,6 +242,8 @@ app.add_middleware(
|
||||
|
||||
_VALID_HANDLE = re.compile(r'^[a-z0-9][a-z0-9_-]{0,29}$')
|
||||
from bincio.edit.ops import VALID_ACTIVITY_ID as _VALID_ACTIVITY_ID
|
||||
from bincio.segments import models as _seg_models
|
||||
from bincio.segments import store as _seg_store
|
||||
_SESSION_COOKIE = "bincio_session"
|
||||
_COOKIE_MAX_AGE = 30 * 86400 # 30 days
|
||||
_SESSION_DOMAIN = os.environ.get("SESSION_DOMAIN") or None # e.g. ".bincio.org" in production
|
||||
@@ -2397,6 +2406,84 @@ async def upload_strava_zip(
|
||||
)
|
||||
|
||||
|
||||
# ── Segments ──────────────────────────────────────────────────────────────────
|
||||
|
||||
@app.get("/api/segments")
|
||||
async def get_segments(
|
||||
bbox: Optional[str] = None,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
"""List segments, optionally filtered to a map viewport bbox (lon_min,lat_min,lon_max,lat_max)."""
|
||||
_require_user(bincio_session)
|
||||
parsed_bbox: Optional[list[float]] = None
|
||||
if bbox:
|
||||
try:
|
||||
parts = [float(x) for x in bbox.split(",")]
|
||||
if len(parts) == 4:
|
||||
parsed_bbox = parts
|
||||
except ValueError:
|
||||
raise HTTPException(400, "bbox must be four comma-separated floats")
|
||||
dd = _get_data_dir()
|
||||
segs = _seg_store.list_segments(dd, parsed_bbox)
|
||||
return JSONResponse([{
|
||||
"id": s.id,
|
||||
"name": s.name,
|
||||
"sport": s.sport,
|
||||
"distance_m": s.distance_m,
|
||||
"bbox": s.bbox,
|
||||
"polyline": s.polyline,
|
||||
"created_by": s.created_by,
|
||||
"created_at": _seg_store._iso(s.created_at),
|
||||
} for s in segs])
|
||||
|
||||
|
||||
@app.post("/api/segments")
|
||||
async def create_segment(
|
||||
body: CreateSegmentRequest,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
if len(body.polyline) < 2:
|
||||
raise HTTPException(400, "polyline must have at least 2 points")
|
||||
if body.distance_m <= 0:
|
||||
raise HTTPException(400, "distance_m must be positive")
|
||||
|
||||
lats = [p[0] for p in body.polyline]
|
||||
lons = [p[1] for p in body.polyline]
|
||||
bbox = [min(lons), min(lats), max(lons), max(lats)]
|
||||
|
||||
seg_id = _seg_store.make_segment_id(body.name)
|
||||
from datetime import datetime, timezone as _tz
|
||||
seg = _seg_models.Segment(
|
||||
id=seg_id,
|
||||
name=body.name,
|
||||
sport=body.sport or None,
|
||||
polyline=body.polyline,
|
||||
distance_m=body.distance_m,
|
||||
bbox=bbox,
|
||||
created_by=user.handle,
|
||||
created_at=datetime.now(_tz.utc),
|
||||
)
|
||||
_seg_store.save_segment(_get_data_dir(), seg)
|
||||
return JSONResponse({"id": seg_id}, status_code=201)
|
||||
|
||||
|
||||
@app.delete("/api/segments/{segment_id}")
|
||||
async def delete_segment(
|
||||
segment_id: str,
|
||||
bincio_session: Optional[str] = Cookie(default=None),
|
||||
) -> JSONResponse:
|
||||
user = _require_user(bincio_session)
|
||||
dd = _get_data_dir()
|
||||
seg = _seg_store.load_segment(dd, segment_id)
|
||||
if seg is None:
|
||||
raise HTTPException(404, "Segment not found")
|
||||
if seg.created_by != user.handle and not user.is_admin:
|
||||
raise HTTPException(403, "Not allowed")
|
||||
_seg_store.delete_segment(dd, segment_id)
|
||||
return JSONResponse({"ok": True})
|
||||
|
||||
|
||||
# ── Feedback ──────────────────────────────────────────────────────────────────
|
||||
|
||||
_FEEDBACK_IMAGE_SUFFIXES = {".jpg", ".jpeg", ".png", ".gif", ".webp", ".heic"}
|
||||
|
||||
Reference in New Issue
Block a user