add homekeeper stack
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
FROM python:3.12-slim
|
||||
WORKDIR /app
|
||||
COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY app/ ./app/
|
||||
EXPOSE 8000
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
@@ -0,0 +1,67 @@
|
||||
import os
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from passlib.context import CryptContext
|
||||
from sqlalchemy import text
|
||||
|
||||
security = HTTPBasic()
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return pwd_context.verify(plain, hashed)
|
||||
|
||||
|
||||
def hash_password(plain: str) -> str:
|
||||
return pwd_context.hash(plain)
|
||||
|
||||
|
||||
def get_current_user(credentials: HTTPBasicCredentials = Depends(security)):
|
||||
from app.database import engine
|
||||
|
||||
with engine.connect() as conn:
|
||||
row = conn.execute(
|
||||
text("SELECT hashed_password FROM auth.users WHERE username = :u"),
|
||||
{"u": credentials.username},
|
||||
).fetchone()
|
||||
|
||||
if row is None or not verify_password(credentials.password, row[0]):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Invalid credentials",
|
||||
headers={"WWW-Authenticate": "Basic"},
|
||||
)
|
||||
return credentials.username
|
||||
|
||||
|
||||
def seed_users():
|
||||
"""Seed initial users from INITIAL_USERS env var if users table is empty."""
|
||||
from app.database import engine
|
||||
|
||||
initial_users_env = os.getenv("INITIAL_USERS", "")
|
||||
if not initial_users_env:
|
||||
return
|
||||
|
||||
with engine.connect() as conn:
|
||||
count = conn.execute(text("SELECT COUNT(*) FROM auth.users")).scalar()
|
||||
if count and count > 0:
|
||||
return
|
||||
|
||||
for pair in initial_users_env.split(","):
|
||||
pair = pair.strip()
|
||||
if ":" not in pair:
|
||||
continue
|
||||
username, password = pair.split(":", 1)
|
||||
username = username.strip()
|
||||
password = password.strip()
|
||||
if not username or not password:
|
||||
continue
|
||||
hashed = hash_password(password)
|
||||
conn.execute(
|
||||
text(
|
||||
"INSERT INTO auth.users (username, hashed_password) "
|
||||
"VALUES (:u, :h) ON CONFLICT (username) DO NOTHING"
|
||||
),
|
||||
{"u": username, "h": hashed},
|
||||
)
|
||||
conn.commit()
|
||||
@@ -0,0 +1,243 @@
|
||||
import os
|
||||
from sqlalchemy import create_engine, text
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
_DB_HOST = os.getenv("DB_HOST", "localhost")
|
||||
_DB_PORT = os.getenv("DB_PORT", "5432")
|
||||
_DB_NAME = os.getenv("DB_NAME", "homestead")
|
||||
_DB_USER = os.getenv("DB_USER", "homestead")
|
||||
_DB_PASSWORD = os.getenv("DB_PASSWORD", "homestead")
|
||||
|
||||
DATABASE_URL = (
|
||||
f"postgresql+psycopg2://{_DB_USER}:{_DB_PASSWORD}@{_DB_HOST}:{_DB_PORT}/{_DB_NAME}"
|
||||
)
|
||||
|
||||
engine = create_engine(DATABASE_URL, pool_pre_ping=True)
|
||||
|
||||
|
||||
def get_session():
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
|
||||
def init_db():
|
||||
"""Create schemas and tables, then seed initial users."""
|
||||
from app.auth import seed_users
|
||||
|
||||
with engine.connect() as conn:
|
||||
# Create auth schema and users table
|
||||
conn.execute(text("CREATE SCHEMA IF NOT EXISTS auth"))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS auth.users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
hashed_password TEXT NOT NULL
|
||||
)
|
||||
"""))
|
||||
|
||||
# Create lk schema and tables
|
||||
conn.execute(text("CREATE SCHEMA IF NOT EXISTS lk"))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS lk.lists (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
type TEXT NOT NULL CHECK (type IN ('shopping', 'todo', 'project')),
|
||||
t_shirt_size TEXT CHECK (t_shirt_size IN ('XS', 'S', 'M', 'L', 'XL')),
|
||||
status TEXT NOT NULL DEFAULT 'open' CHECK (status IN ('open', 'closed')),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT now(),
|
||||
closed_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS lk.list_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
list_id INTEGER NOT NULL REFERENCES lk.lists(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
checked BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
position INTEGER NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ DEFAULT now()
|
||||
)
|
||||
"""))
|
||||
|
||||
# Create public schema beekeeper tables
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS apiaries (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
location_text TEXT,
|
||||
lat REAL,
|
||||
lng REAL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS colonies (
|
||||
id SERIAL PRIMARY KEY,
|
||||
apiary_id INTEGER REFERENCES apiaries(id),
|
||||
name TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'wirtschaftsvolk',
|
||||
parent_colony_id INTEGER REFERENCES colonies(id),
|
||||
current_queen_id INTEGER,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
closed_at TIMESTAMPTZ
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS colony_locations (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
apiary_id INTEGER NOT NULL REFERENCES apiaries(id),
|
||||
moved_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS queens (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER REFERENCES colonies(id),
|
||||
year INTEGER,
|
||||
breed TEXT,
|
||||
origin_colony_id INTEGER REFERENCES colonies(id),
|
||||
marked_color TEXT,
|
||||
notes TEXT,
|
||||
introduced_at TEXT,
|
||||
superseded_at TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS inspections (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
date TEXT NOT NULL,
|
||||
inspector TEXT,
|
||||
brood_frames INTEGER,
|
||||
honey_frames INTEGER,
|
||||
food_stores_kg REAL,
|
||||
colony_strength INTEGER,
|
||||
queen_seen INTEGER NOT NULL DEFAULT 0,
|
||||
eggs_seen INTEGER NOT NULL DEFAULT 0,
|
||||
queen_cells_count INTEGER NOT NULL DEFAULT 0,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS inspection_traits (
|
||||
id SERIAL PRIMARY KEY,
|
||||
inspection_id INTEGER NOT NULL REFERENCES inspections(id) ON DELETE CASCADE,
|
||||
trait TEXT NOT NULL,
|
||||
score INTEGER NOT NULL CHECK (score BETWEEN 1 AND 4)
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS varroa_counts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
date_from TEXT,
|
||||
date TEXT NOT NULL,
|
||||
time TEXT,
|
||||
method TEXT NOT NULL,
|
||||
count REAL NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS varroa_treatments (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
date TEXT NOT NULL,
|
||||
time TEXT,
|
||||
product TEXT NOT NULL,
|
||||
method TEXT,
|
||||
dosage TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS varroa_controls (
|
||||
id SERIAL PRIMARY KEY,
|
||||
treatment_id INTEGER NOT NULL REFERENCES varroa_treatments(id) ON DELETE CASCADE,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
date TEXT NOT NULL,
|
||||
time TEXT,
|
||||
result TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS colony_actions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER NOT NULL REFERENCES colonies(id),
|
||||
date TEXT NOT NULL,
|
||||
time TEXT,
|
||||
type TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
related_colony_id INTEGER REFERENCES colonies(id),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS honey_harvests (
|
||||
id SERIAL PRIMARY KEY,
|
||||
colony_id INTEGER REFERENCES colonies(id),
|
||||
apiary_id INTEGER REFERENCES apiaries(id),
|
||||
date TEXT NOT NULL,
|
||||
time TEXT,
|
||||
weight_kg REAL NOT NULL,
|
||||
type TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS checklists (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
scope TEXT NOT NULL DEFAULT 'colony',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS checklist_items (
|
||||
id SERIAL PRIMARY KEY,
|
||||
checklist_id INTEGER NOT NULL REFERENCES checklists(id) ON DELETE CASCADE,
|
||||
text TEXT NOT NULL,
|
||||
position INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"""))
|
||||
conn.execute(text("""
|
||||
CREATE TABLE IF NOT EXISTS checklist_completions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
checklist_id INTEGER NOT NULL REFERENCES checklists(id),
|
||||
item_id INTEGER NOT NULL REFERENCES checklist_items(id),
|
||||
scope_id INTEGER NOT NULL,
|
||||
date TEXT NOT NULL,
|
||||
completed_by TEXT,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""))
|
||||
|
||||
# Add columns that may be missing from older installs (idempotent)
|
||||
for stmt in [
|
||||
"ALTER TABLE varroa_counts ADD COLUMN IF NOT EXISTS date_from TEXT",
|
||||
"ALTER TABLE varroa_counts ADD COLUMN IF NOT EXISTS time TEXT",
|
||||
"ALTER TABLE varroa_treatments ADD COLUMN IF NOT EXISTS time TEXT",
|
||||
"ALTER TABLE varroa_controls ADD COLUMN IF NOT EXISTS time TEXT",
|
||||
"ALTER TABLE honey_harvests ADD COLUMN IF NOT EXISTS time TEXT",
|
||||
"ALTER TABLE colony_actions ADD COLUMN IF NOT EXISTS related_colony_id INTEGER REFERENCES colonies(id)",
|
||||
]:
|
||||
try:
|
||||
conn.execute(text(stmt))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
conn.commit()
|
||||
|
||||
seed_users()
|
||||
@@ -0,0 +1,45 @@
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
|
||||
from app.routers import (
|
||||
apiaries,
|
||||
colonies,
|
||||
inspections,
|
||||
varroa,
|
||||
harvests,
|
||||
actions,
|
||||
queens,
|
||||
checklists,
|
||||
lists,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
from app.database import init_db
|
||||
init_db()
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title="Beekeeper API",
|
||||
version="1.0.0",
|
||||
root_path=os.getenv("ROOT_PATH", ""),
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.include_router(apiaries.router, prefix="/v1/apiaries", tags=["apiaries"])
|
||||
app.include_router(colonies.router, prefix="/v1/colonies", tags=["colonies"])
|
||||
app.include_router(inspections.router, prefix="/v1/inspections", tags=["inspections"])
|
||||
app.include_router(varroa.router, prefix="/v1/varroa", tags=["varroa"])
|
||||
app.include_router(harvests.router, prefix="/v1/harvests", tags=["harvests"])
|
||||
app.include_router(actions.router, prefix="/v1/actions", tags=["actions"])
|
||||
app.include_router(queens.router, prefix="/v1/queens", tags=["queens"])
|
||||
app.include_router(checklists.router, prefix="/v1/checklists", tags=["checklists"])
|
||||
app.include_router(lists.router, prefix="/v1/lists", tags=["lists"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {"status": "ok"}
|
||||
@@ -0,0 +1,10 @@
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class User(SQLModel, table=True):
|
||||
__tablename__ = "users"
|
||||
__table_args__ = {"schema": "auth"}
|
||||
|
||||
id: int | None = Field(default=None, primary_key=True)
|
||||
username: str = Field(unique=True, index=True)
|
||||
hashed_password: str
|
||||
@@ -0,0 +1,94 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_actions(
|
||||
colony_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM colony_actions WHERE colony_id = :colony_id
|
||||
ORDER BY date::DATE DESC, created_at DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_action(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO colony_actions (colony_id, date, time, type, notes, related_colony_id)
|
||||
VALUES (:colony_id, :date, :time, :type, :notes, :related_colony_id)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"colony_id": body.get("colony_id"),
|
||||
"date": body.get("date"),
|
||||
"time": body.get("time"),
|
||||
"type": body.get("type"),
|
||||
"notes": body.get("notes"),
|
||||
"related_colony_id": body.get("related_colony_id"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_action(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM colony_actions WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_action(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE colony_actions SET date = :date, type = :type, notes = :notes
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date": body.get("date"),
|
||||
"type": body.get("type"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_action(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM colony_actions WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,99 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_apiaries(
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT a.*, COUNT(c.id) AS colony_count
|
||||
FROM apiaries a
|
||||
LEFT JOIN colonies c ON c.apiary_id = a.id AND c.closed_at IS NULL
|
||||
GROUP BY a.id
|
||||
ORDER BY a.name
|
||||
""")).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_apiary(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM apiaries WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_apiary(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO apiaries (name, location_text, lat, lng, notes)
|
||||
VALUES (:name, :location_text, :lat, :lng, :notes)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"name": body.get("name"),
|
||||
"location_text": body.get("location_text"),
|
||||
"lat": body.get("lat"),
|
||||
"lng": body.get("lng"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_apiary(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE apiaries
|
||||
SET name = :name, location_text = :location_text, lat = :lat, lng = :lng, notes = :notes
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"name": body.get("name"),
|
||||
"location_text": body.get("location_text"),
|
||||
"lat": body.get("lat"),
|
||||
"lng": body.get("lng"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_apiary(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM apiaries WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,144 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_checklists(
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("SELECT * FROM checklists ORDER BY name")).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_checklist(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO checklists (name, scope) VALUES (:name, :scope) RETURNING *
|
||||
"""), {"name": body.get("name"), "scope": body.get("scope", "colony")}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_checklist(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM checklists WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/{id}/items")
|
||||
def list_items(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM checklist_items WHERE checklist_id = :id ORDER BY position
|
||||
"""), {"id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/{id}/items", status_code=201)
|
||||
def create_item(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO checklist_items (checklist_id, text, position)
|
||||
VALUES (:checklist_id, :text, :position) RETURNING *
|
||||
"""), {
|
||||
"checklist_id": id,
|
||||
"text": body.get("text"),
|
||||
"position": body.get("position", 0),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.delete("/items/{item_id}", status_code=204)
|
||||
def delete_item(
|
||||
item_id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM checklist_items WHERE id = :id"), {"id": item_id})
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/{id}/completions")
|
||||
def list_completions(
|
||||
id: int,
|
||||
scope_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT cc.*, ci.text, ci.position
|
||||
FROM checklist_completions cc
|
||||
JOIN checklist_items ci ON ci.id = cc.item_id
|
||||
WHERE cc.checklist_id = :checklist_id AND cc.scope_id = :scope_id
|
||||
ORDER BY ci.position
|
||||
"""), {"checklist_id": id, "scope_id": scope_id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/{id}/completions/toggle")
|
||||
def toggle_completion(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
item_id = body.get("item_id")
|
||||
scope_id = body.get("scope_id")
|
||||
date = body.get("date")
|
||||
completed_by = body.get("completed_by")
|
||||
notes = body.get("notes")
|
||||
|
||||
existing = session.execute(text("""
|
||||
SELECT id FROM checklist_completions
|
||||
WHERE checklist_id = :checklist_id AND item_id = :item_id AND scope_id = :scope_id
|
||||
"""), {"checklist_id": id, "item_id": item_id, "scope_id": scope_id}).fetchone()
|
||||
|
||||
if existing is not None:
|
||||
session.execute(
|
||||
text("DELETE FROM checklist_completions WHERE id = :id"),
|
||||
{"id": existing._mapping["id"]},
|
||||
)
|
||||
session.commit()
|
||||
return {"toggled": False}
|
||||
else:
|
||||
session.execute(text("""
|
||||
INSERT INTO checklist_completions (checklist_id, item_id, scope_id, date, completed_by, notes)
|
||||
VALUES (:checklist_id, :item_id, :scope_id, :date, :completed_by, :notes)
|
||||
"""), {
|
||||
"checklist_id": id,
|
||||
"item_id": item_id,
|
||||
"scope_id": scope_id,
|
||||
"date": date,
|
||||
"completed_by": completed_by,
|
||||
"notes": notes,
|
||||
})
|
||||
session.commit()
|
||||
return {"toggled": True}
|
||||
@@ -0,0 +1,294 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_colonies(
|
||||
apiary_id: int | None = Query(default=None),
|
||||
include_closed: bool = Query(default=False),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
where = "1=1" if include_closed else "c.closed_at IS NULL"
|
||||
params: dict[str, Any] = {}
|
||||
if apiary_id is not None:
|
||||
where += " AND c.apiary_id = :apiary_id"
|
||||
params["apiary_id"] = apiary_id
|
||||
|
||||
rows = session.execute(text(f"""
|
||||
SELECT
|
||||
c.*,
|
||||
a.name AS apiary_name,
|
||||
p.name AS parent_name,
|
||||
q.year AS queen_year,
|
||||
q.breed AS queen_breed,
|
||||
(SELECT MAX(i.date::DATE)
|
||||
FROM inspections i
|
||||
WHERE i.colony_id = c.id) AS last_inspection_date
|
||||
FROM colonies c
|
||||
LEFT JOIN apiaries a ON a.id = c.apiary_id
|
||||
LEFT JOIN colonies p ON p.id = c.parent_colony_id
|
||||
LEFT JOIN queens q ON q.id = c.current_queen_id
|
||||
WHERE {where}
|
||||
ORDER BY a.name, c.name
|
||||
"""), params).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/log-placeholder", include_in_schema=False)
|
||||
def _log_placeholder():
|
||||
pass # just to force ordering; actual log route is below
|
||||
|
||||
|
||||
@router.get("/{id}/log")
|
||||
def colony_log(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT date, type, summary, ref_id, created_at FROM (
|
||||
SELECT date, 'durchsicht' AS type,
|
||||
TRIM(BOTH FROM CONCAT_WS(' ',
|
||||
CASE WHEN brood_frames IS NOT NULL THEN brood_frames::TEXT || ' BW' END,
|
||||
CASE WHEN honey_frames IS NOT NULL THEN honey_frames::TEXT || ' HW' END,
|
||||
CASE WHEN colony_strength IS NOT NULL THEN 'Stärke ' || colony_strength::TEXT END,
|
||||
CASE WHEN queen_seen = 1 THEN '♛ gesehen' END
|
||||
)) AS summary,
|
||||
id AS ref_id, created_at
|
||||
FROM inspections WHERE colony_id = :colony_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT date, 'kontrolle' AS type,
|
||||
count::TEXT || ' Milben' ||
|
||||
CASE WHEN date_from IS NOT NULL
|
||||
THEN ' · ' || (date::DATE - date_from::DATE)::TEXT || ' Tage' ELSE '' END AS summary,
|
||||
id AS ref_id, created_at
|
||||
FROM varroa_counts WHERE colony_id = :colony_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT date, 'behandlung' AS type,
|
||||
product || CASE WHEN dosage IS NOT NULL AND dosage <> ''
|
||||
THEN ' · ' || dosage ELSE '' END AS summary,
|
||||
id AS ref_id, created_at
|
||||
FROM varroa_treatments WHERE colony_id = :colony_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT vc.date, 'nachkontrolle' AS type,
|
||||
vt.product || CASE WHEN vc.result IS NOT NULL AND vc.result <> ''
|
||||
THEN ': ' || vc.result ELSE '' END AS summary,
|
||||
vc.id AS ref_id, vc.created_at
|
||||
FROM varroa_controls vc
|
||||
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
|
||||
WHERE vc.colony_id = :colony_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT h.date, 'ernte' AS type,
|
||||
h.weight_kg::TEXT || ' kg' ||
|
||||
CASE WHEN h.type IS NOT NULL THEN ' · ' || h.type ELSE '' END AS summary,
|
||||
h.id AS ref_id, h.created_at
|
||||
FROM honey_harvests h WHERE h.colony_id = :colony_id
|
||||
|
||||
UNION ALL
|
||||
|
||||
SELECT ca.date, 'massnahme' AS type,
|
||||
CASE ca.type
|
||||
WHEN 'ablegerbildung' THEN 'Ablegerbildung'
|
||||
WHEN 'zwischenbodenableger' THEN 'Zwischenbödenableger'
|
||||
WHEN 'brutentnahme' THEN 'Brutentnahme'
|
||||
WHEN 'standwechsel' THEN 'Standwechsel'
|
||||
WHEN 'futterung' THEN 'Fütterung'
|
||||
WHEN 'vereinigung' THEN 'Völkervereinigung'
|
||||
ELSE 'Maßnahme'
|
||||
END ||
|
||||
CASE WHEN ca.notes IS NOT NULL AND ca.notes <> ''
|
||||
THEN ': ' || ca.notes ELSE '' END AS summary,
|
||||
ca.id AS ref_id, ca.created_at
|
||||
FROM colony_actions ca WHERE ca.colony_id = :colony_id
|
||||
) log
|
||||
ORDER BY date::DATE DESC, created_at DESC
|
||||
"""), {"colony_id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}/lineage")
|
||||
def colony_lineage(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
WITH RECURSIVE lineage(id, name, status, parent_colony_id, depth) AS (
|
||||
SELECT id, name, status, parent_colony_id, 0
|
||||
FROM colonies WHERE id = :colony_id
|
||||
UNION ALL
|
||||
SELECT c.id, c.name, c.status, c.parent_colony_id, l.depth + 1
|
||||
FROM colonies c
|
||||
JOIN lineage l ON c.id = l.parent_colony_id
|
||||
)
|
||||
SELECT * FROM lineage ORDER BY depth DESC
|
||||
"""), {"colony_id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}/descendants")
|
||||
def colony_descendants(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
WITH RECURSIVE descendants(id, name, status, parent_colony_id, depth) AS (
|
||||
SELECT id, name, status, parent_colony_id, 0
|
||||
FROM colonies WHERE id = :colony_id
|
||||
UNION ALL
|
||||
SELECT c.id, c.name, c.status, c.parent_colony_id, d.depth + 1
|
||||
FROM colonies c
|
||||
JOIN descendants d ON c.parent_colony_id = d.id
|
||||
)
|
||||
SELECT * FROM descendants WHERE depth > 0 ORDER BY depth, name
|
||||
"""), {"colony_id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}/location-history")
|
||||
def colony_location_history(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT cl.*, a.name AS apiary_name
|
||||
FROM colony_locations cl
|
||||
JOIN apiaries a ON a.id = cl.apiary_id
|
||||
WHERE cl.colony_id = :colony_id
|
||||
ORDER BY cl.moved_at DESC
|
||||
"""), {"colony_id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_colony(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
SELECT c.*, a.name AS apiary_name, p.name AS parent_name
|
||||
FROM colonies c
|
||||
LEFT JOIN apiaries a ON a.id = c.apiary_id
|
||||
LEFT JOIN colonies p ON p.id = c.parent_colony_id
|
||||
WHERE c.id = :id
|
||||
"""), {"id": id}).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_colony(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO colonies (name, apiary_id, status, parent_colony_id, notes)
|
||||
VALUES (:name, :apiary_id, :status, :parent_colony_id, :notes)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"name": body.get("name"),
|
||||
"apiary_id": body.get("apiary_id"),
|
||||
"status": body.get("status", "wirtschaftsvolk"),
|
||||
"parent_colony_id": body.get("parent_colony_id"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
colony_id = row._mapping["id"]
|
||||
session.execute(text("""
|
||||
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
|
||||
"""), {"colony_id": colony_id, "apiary_id": body.get("apiary_id")})
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_colony(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
# Check if apiary changed (to insert colony_location)
|
||||
old = session.execute(
|
||||
text("SELECT apiary_id FROM colonies WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
|
||||
result = session.execute(text("""
|
||||
UPDATE colonies
|
||||
SET name = :name, apiary_id = :apiary_id, status = :status, notes = :notes
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"name": body.get("name"),
|
||||
"apiary_id": body.get("apiary_id"),
|
||||
"status": body.get("status"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
new_apiary_id = body.get("apiary_id")
|
||||
if old is not None and old._mapping["apiary_id"] != new_apiary_id:
|
||||
session.execute(text("""
|
||||
INSERT INTO colony_locations (colony_id, apiary_id) VALUES (:colony_id, :apiary_id)
|
||||
"""), {"colony_id": id, "apiary_id": new_apiary_id})
|
||||
|
||||
session.commit()
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.post("/{id}/close")
|
||||
def close_colony(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE colonies SET status = 'eingegangen', closed_at = NOW()
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {"id": id}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.post("/{id}/set-queen")
|
||||
def set_queen(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE colonies SET current_queen_id = :queen_id WHERE id = :id RETURNING *
|
||||
"""), {"id": id, "queen_id": body.get("queen_id")}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
@@ -0,0 +1,136 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("/summary")
|
||||
def harvest_summary(
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT
|
||||
EXTRACT(YEAR FROM date::date)::INTEGER AS year,
|
||||
type,
|
||||
SUM(weight_kg) AS total_kg,
|
||||
COUNT(*) AS n_harvests
|
||||
FROM honey_harvests
|
||||
GROUP BY year, type
|
||||
ORDER BY year DESC, total_kg DESC
|
||||
""")).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_harvests(
|
||||
colony_id: int | None = Query(default=None),
|
||||
apiary_id: int | None = Query(default=None),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
if colony_id is not None:
|
||||
rows = session.execute(text("""
|
||||
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
|
||||
FROM honey_harvests h
|
||||
LEFT JOIN colonies c ON c.id = h.colony_id
|
||||
LEFT JOIN apiaries a ON a.id = h.apiary_id
|
||||
WHERE h.colony_id = :colony_id
|
||||
ORDER BY h.date DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
elif apiary_id is not None:
|
||||
rows = session.execute(text("""
|
||||
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
|
||||
FROM honey_harvests h
|
||||
LEFT JOIN colonies c ON c.id = h.colony_id
|
||||
LEFT JOIN apiaries a ON a.id = h.apiary_id
|
||||
WHERE h.apiary_id = :apiary_id
|
||||
ORDER BY h.date DESC
|
||||
"""), {"apiary_id": apiary_id}).fetchall()
|
||||
else:
|
||||
rows = session.execute(text("""
|
||||
SELECT h.*, c.name AS colony_name, a.name AS apiary_name
|
||||
FROM honey_harvests h
|
||||
LEFT JOIN colonies c ON c.id = h.colony_id
|
||||
LEFT JOIN apiaries a ON a.id = h.apiary_id
|
||||
ORDER BY h.date DESC
|
||||
""")).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_harvest(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO honey_harvests (date, weight_kg, colony_id, apiary_id, type, notes, time)
|
||||
VALUES (:date, :weight_kg, :colony_id, :apiary_id, :type, :notes, :time)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"date": body.get("date"),
|
||||
"weight_kg": body.get("weight_kg"),
|
||||
"colony_id": body.get("colony_id"),
|
||||
"apiary_id": body.get("apiary_id"),
|
||||
"type": body.get("type"),
|
||||
"notes": body.get("notes"),
|
||||
"time": body.get("time"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_harvest(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM honey_harvests WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_harvest(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE honey_harvests SET date = :date, weight_kg = :weight_kg, notes = :notes
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date": body.get("date"),
|
||||
"weight_kg": body.get("weight_kg"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_harvest(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM honey_harvests WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,164 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_inspections(
|
||||
colony_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM inspections WHERE colony_id = :colony_id
|
||||
ORDER BY date::DATE DESC, id DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/last")
|
||||
def last_inspection(
|
||||
colony_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
row = session.execute(text("""
|
||||
SELECT * FROM inspections WHERE colony_id = :colony_id
|
||||
ORDER BY date::DATE DESC, id DESC LIMIT 1
|
||||
"""), {"colony_id": colony_id}).fetchone()
|
||||
if row is None:
|
||||
return None
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/{id}/traits")
|
||||
def inspection_traits(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM inspection_traits WHERE inspection_id = :id
|
||||
"""), {"id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_inspection(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM inspections WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_inspection(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO inspections
|
||||
(colony_id, date, inspector, brood_frames, honey_frames, food_stores_kg,
|
||||
colony_strength, queen_seen, eggs_seen, queen_cells_count, notes)
|
||||
VALUES
|
||||
(:colony_id, :date, :inspector, :brood_frames, :honey_frames, :food_stores_kg,
|
||||
:colony_strength, :queen_seen, :eggs_seen, :queen_cells_count, :notes)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"colony_id": body.get("colony_id"),
|
||||
"date": body.get("date"),
|
||||
"inspector": body.get("inspector"),
|
||||
"brood_frames": body.get("brood_frames"),
|
||||
"honey_frames": body.get("honey_frames"),
|
||||
"food_stores_kg": body.get("food_stores_kg"),
|
||||
"colony_strength": body.get("colony_strength"),
|
||||
"queen_seen": int(body.get("queen_seen", 0)),
|
||||
"eggs_seen": int(body.get("eggs_seen", 0)),
|
||||
"queen_cells_count": int(body.get("queen_cells_count", 0)),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
inspection_id = row._mapping["id"]
|
||||
|
||||
traits = body.get("traits", {})
|
||||
for trait, score in (traits or {}).items():
|
||||
if score is not None:
|
||||
session.execute(text("""
|
||||
INSERT INTO inspection_traits (inspection_id, trait, score)
|
||||
VALUES (:inspection_id, :trait, :score)
|
||||
"""), {"inspection_id": inspection_id, "trait": trait, "score": int(score)})
|
||||
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_inspection(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE inspections SET
|
||||
date = :date, inspector = :inspector, brood_frames = :brood_frames,
|
||||
honey_frames = :honey_frames, food_stores_kg = :food_stores_kg,
|
||||
colony_strength = :colony_strength, queen_seen = :queen_seen,
|
||||
eggs_seen = :eggs_seen, queen_cells_count = :queen_cells_count, notes = :notes
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date": body.get("date"),
|
||||
"inspector": body.get("inspector"),
|
||||
"brood_frames": body.get("brood_frames"),
|
||||
"honey_frames": body.get("honey_frames"),
|
||||
"food_stores_kg": body.get("food_stores_kg"),
|
||||
"colony_strength": body.get("colony_strength"),
|
||||
"queen_seen": int(body.get("queen_seen", 0)),
|
||||
"eggs_seen": int(body.get("eggs_seen", 0)),
|
||||
"queen_cells_count": int(body.get("queen_cells_count", 0)),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
session.execute(
|
||||
text("DELETE FROM inspection_traits WHERE inspection_id = :id"), {"id": id}
|
||||
)
|
||||
traits = body.get("traits", {})
|
||||
for trait, score in (traits or {}).items():
|
||||
if score is not None:
|
||||
session.execute(text("""
|
||||
INSERT INTO inspection_traits (inspection_id, trait, score)
|
||||
VALUES (:inspection_id, :trait, :score)
|
||||
"""), {"inspection_id": id, "trait": trait, "score": int(score)})
|
||||
|
||||
session.commit()
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_inspection(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM inspections WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,149 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_lists(
|
||||
include_closed: bool = Query(default=False),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
where = "" if include_closed else "WHERE l.closed_at IS NULL"
|
||||
order = "ORDER BY l.closed_at IS NOT NULL, l.created_at DESC" if include_closed else "ORDER BY l.created_at DESC"
|
||||
rows = session.execute(text(f"""
|
||||
SELECT
|
||||
l.*,
|
||||
COUNT(i.id) AS item_count,
|
||||
COUNT(i.id) FILTER (WHERE i.checked) AS checked_count
|
||||
FROM lk.lists l
|
||||
LEFT JOIN lk.list_items i ON i.list_id = l.id
|
||||
{where}
|
||||
GROUP BY l.id
|
||||
{order}
|
||||
""")).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_list(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO lk.lists (name, type, t_shirt_size, notes)
|
||||
VALUES (:name, :type, :t_shirt_size, :notes)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"name": body.get("name"),
|
||||
"type": body.get("type"),
|
||||
"t_shirt_size": body.get("t_shirt_size"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.post("/{id}/close")
|
||||
def close_list(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE lk.lists SET status = 'closed', closed_at = now()
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {"id": id}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.post("/{id}/reopen")
|
||||
def reopen_list(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE lk.lists SET status = 'open', closed_at = NULL
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {"id": id}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_list(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM lk.lists WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
|
||||
|
||||
@router.get("/{id}/items")
|
||||
def list_items(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM lk.list_items WHERE list_id = :list_id ORDER BY position, created_at
|
||||
"""), {"list_id": id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/{id}/items", status_code=201)
|
||||
def create_item(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO lk.list_items (list_id, text) VALUES (:list_id, :text) RETURNING *
|
||||
"""), {"list_id": id, "text": body.get("text")}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.patch("/items/{id}")
|
||||
def update_item_checked(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE lk.list_items SET checked = :checked WHERE id = :id RETURNING *
|
||||
"""), {"id": id, "checked": bool(body.get("checked", False))}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/items/{id}", status_code=204)
|
||||
def delete_item(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM lk.list_items WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,141 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
@router.get("")
|
||||
def list_queens(
|
||||
colony_id: int | None = Query(default=None),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
params: dict[str, Any] = {}
|
||||
where = ""
|
||||
if colony_id is not None:
|
||||
where = "WHERE q.colony_id = :colony_id"
|
||||
params["colony_id"] = colony_id
|
||||
|
||||
rows = session.execute(text(f"""
|
||||
SELECT q.*, c.name AS colony_name, o.name AS origin_colony_name
|
||||
FROM queens q
|
||||
LEFT JOIN colonies c ON c.id = q.colony_id
|
||||
LEFT JOIN colonies o ON o.id = q.origin_colony_id
|
||||
{where}
|
||||
ORDER BY q.introduced_at DESC
|
||||
"""), params).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("", status_code=201)
|
||||
def create_queen(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO queens (colony_id, year, breed, origin_colony_id, marked_color, notes, introduced_at)
|
||||
VALUES (:colony_id, :year, :breed, :origin_colony_id, :marked_color, :notes, :introduced_at)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"colony_id": body.get("colony_id"),
|
||||
"year": body.get("year"),
|
||||
"breed": body.get("breed"),
|
||||
"origin_colony_id": body.get("origin_colony_id"),
|
||||
"marked_color": body.get("marked_color"),
|
||||
"notes": body.get("notes"),
|
||||
"introduced_at": body.get("introduced_at"),
|
||||
}).fetchone()
|
||||
queen_id = row._mapping["id"]
|
||||
colony_id = body.get("colony_id")
|
||||
if colony_id is not None:
|
||||
session.execute(text("""
|
||||
UPDATE colonies SET current_queen_id = :queen_id WHERE id = :colony_id
|
||||
"""), {"queen_id": queen_id, "colony_id": colony_id})
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/{id}")
|
||||
def get_queen(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM queens WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/{id}")
|
||||
def update_queen(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE queens SET
|
||||
colony_id = :colony_id, year = :year, breed = :breed,
|
||||
origin_colony_id = :origin_colony_id, marked_color = :marked_color,
|
||||
notes = :notes, introduced_at = :introduced_at, superseded_at = :superseded_at
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"colony_id": body.get("colony_id"),
|
||||
"year": body.get("year"),
|
||||
"breed": body.get("breed"),
|
||||
"origin_colony_id": body.get("origin_colony_id"),
|
||||
"marked_color": body.get("marked_color"),
|
||||
"notes": body.get("notes"),
|
||||
"introduced_at": body.get("introduced_at"),
|
||||
"superseded_at": body.get("superseded_at"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.post("/{id}/supersede")
|
||||
def supersede_queen(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE queens SET superseded_at = CURRENT_DATE WHERE id = :id RETURNING *
|
||||
"""), {"id": id}).fetchone()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
colony_id = result._mapping.get("colony_id")
|
||||
if colony_id is not None:
|
||||
session.execute(text("""
|
||||
UPDATE colonies SET current_queen_id = NULL
|
||||
WHERE id = :colony_id AND current_queen_id = :queen_id
|
||||
"""), {"colony_id": colony_id, "queen_id": id})
|
||||
session.commit()
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/{id}", status_code=204)
|
||||
def delete_queen(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM queens WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,284 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from sqlalchemy import text
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Any
|
||||
|
||||
from app.database import get_session
|
||||
from app.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
def _row(r) -> dict:
|
||||
return dict(r._mapping)
|
||||
|
||||
|
||||
# ---- Counts ----
|
||||
|
||||
@router.get("/counts")
|
||||
def list_counts(
|
||||
colony_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM varroa_counts WHERE colony_id = :colony_id ORDER BY date DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/counts", status_code=201)
|
||||
def create_count(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO varroa_counts (colony_id, date_from, date, method, count, notes, time)
|
||||
VALUES (:colony_id, :date_from, :date, :method, :count, :notes, :time)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"colony_id": body.get("colony_id"),
|
||||
"date_from": body.get("date_from"),
|
||||
"date": body.get("date"),
|
||||
"method": body.get("method"),
|
||||
"count": body.get("count"),
|
||||
"notes": body.get("notes"),
|
||||
"time": body.get("time"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/counts/{id}")
|
||||
def get_count(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM varroa_counts WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/counts/{id}")
|
||||
def update_count(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE varroa_counts
|
||||
SET date_from = :date_from, date = :date, method = :method, count = :count, notes = :notes
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date_from": body.get("date_from"),
|
||||
"date": body.get("date"),
|
||||
"method": body.get("method"),
|
||||
"count": body.get("count"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/counts/{id}", status_code=204)
|
||||
def delete_count(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM varroa_counts WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
|
||||
|
||||
# ---- Treatments ----
|
||||
|
||||
@router.get("/treatments")
|
||||
def list_treatments(
|
||||
colony_id: int = Query(...),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
rows = session.execute(text("""
|
||||
SELECT * FROM varroa_treatments WHERE colony_id = :colony_id ORDER BY date DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/treatments", status_code=201)
|
||||
def create_treatment(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO varroa_treatments (colony_id, date, product, method, dosage, notes, time)
|
||||
VALUES (:colony_id, :date, :product, :method, :dosage, :notes, :time)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"colony_id": body.get("colony_id"),
|
||||
"date": body.get("date"),
|
||||
"product": body.get("product"),
|
||||
"method": body.get("method"),
|
||||
"dosage": body.get("dosage"),
|
||||
"notes": body.get("notes"),
|
||||
"time": body.get("time"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/treatments/{id}")
|
||||
def get_treatment(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM varroa_treatments WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/treatments/{id}")
|
||||
def update_treatment(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE varroa_treatments
|
||||
SET date = :date, product = :product, method = :method, dosage = :dosage, notes = :notes
|
||||
WHERE id = :id
|
||||
RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date": body.get("date"),
|
||||
"product": body.get("product"),
|
||||
"method": body.get("method"),
|
||||
"dosage": body.get("dosage"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/treatments/{id}", status_code=204)
|
||||
def delete_treatment(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM varroa_treatments WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
|
||||
|
||||
# ---- Controls ----
|
||||
|
||||
@router.get("/controls")
|
||||
def list_controls(
|
||||
colony_id: int | None = Query(default=None),
|
||||
treatment_id: int | None = Query(default=None),
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> list[dict]:
|
||||
if treatment_id is not None:
|
||||
rows = session.execute(text("""
|
||||
SELECT vc.*, vt.product, vt.date AS treatment_date
|
||||
FROM varroa_controls vc
|
||||
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
|
||||
WHERE vc.treatment_id = :treatment_id
|
||||
ORDER BY vc.date DESC
|
||||
"""), {"treatment_id": treatment_id}).fetchall()
|
||||
elif colony_id is not None:
|
||||
rows = session.execute(text("""
|
||||
SELECT vc.*, vt.product, vt.date AS treatment_date
|
||||
FROM varroa_controls vc
|
||||
JOIN varroa_treatments vt ON vt.id = vc.treatment_id
|
||||
WHERE vc.colony_id = :colony_id
|
||||
ORDER BY vc.date DESC
|
||||
"""), {"colony_id": colony_id}).fetchall()
|
||||
else:
|
||||
return []
|
||||
return [_row(r) for r in rows]
|
||||
|
||||
|
||||
@router.post("/controls", status_code=201)
|
||||
def create_control(
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(text("""
|
||||
INSERT INTO varroa_controls (treatment_id, colony_id, date, result, notes)
|
||||
VALUES (:treatment_id, :colony_id, :date, :result, :notes)
|
||||
RETURNING *
|
||||
"""), {
|
||||
"treatment_id": body.get("treatment_id"),
|
||||
"colony_id": body.get("colony_id"),
|
||||
"date": body.get("date"),
|
||||
"result": body.get("result"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.get("/controls/{id}")
|
||||
def get_control(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
row = session.execute(
|
||||
text("SELECT * FROM varroa_controls WHERE id = :id"), {"id": id}
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(row)
|
||||
|
||||
|
||||
@router.put("/controls/{id}")
|
||||
def update_control(
|
||||
id: int,
|
||||
body: dict[str, Any],
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
) -> dict:
|
||||
result = session.execute(text("""
|
||||
UPDATE varroa_controls SET date = :date, result = :result, notes = :notes
|
||||
WHERE id = :id RETURNING *
|
||||
"""), {
|
||||
"id": id,
|
||||
"date": body.get("date"),
|
||||
"result": body.get("result"),
|
||||
"notes": body.get("notes"),
|
||||
}).fetchone()
|
||||
session.commit()
|
||||
if result is None:
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
return _row(result)
|
||||
|
||||
|
||||
@router.delete("/controls/{id}", status_code=204)
|
||||
def delete_control(
|
||||
id: int,
|
||||
session: Session = Depends(get_session),
|
||||
_: str = Depends(get_current_user),
|
||||
):
|
||||
session.execute(text("DELETE FROM varroa_controls WHERE id = :id"), {"id": id})
|
||||
session.commit()
|
||||
@@ -0,0 +1,7 @@
|
||||
fastapi>=0.115
|
||||
uvicorn[standard]>=0.30
|
||||
sqlmodel>=0.0.21
|
||||
psycopg2-binary>=2.9
|
||||
passlib[bcrypt]>=1.7
|
||||
bcrypt>=4.0,<5.0
|
||||
python-multipart>=0.0.9
|
||||
Reference in New Issue
Block a user