avant codex
This commit is contained in:
@@ -4,4 +4,4 @@ COPY requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
COPY . .
|
||||
RUN mkdir -p /data/uploads
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8060"]
|
||||
|
||||
10
backend/app/models/dicton.py
Normal file
10
backend/app/models/dicton.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class Dicton(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
mois: int # 1-12
|
||||
jour: Optional[int] = None
|
||||
texte: str
|
||||
region: Optional[str] = None # Auvergne|Haute-Loire|National
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
@@ -16,10 +16,12 @@ class Garden(SQLModel, table=True):
|
||||
ombre: Optional[str] = None # ombre | mi-ombre | plein_soleil
|
||||
sol_type: Optional[str] = None
|
||||
sol_ph: Optional[float] = None
|
||||
surface_m2: Optional[float] = None
|
||||
ensoleillement: Optional[str] = None
|
||||
grille_largeur: int = 6
|
||||
grille_hauteur: int = 4
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class GardenCell(SQLModel, table=True):
|
||||
@@ -39,7 +41,7 @@ class GardenImage(SQLModel, table=True):
|
||||
garden_id: int = Field(foreign_key="garden.id", index=True)
|
||||
filename: str
|
||||
caption: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class Measurement(SQLModel, table=True):
|
||||
@@ -50,4 +52,4 @@ class Measurement(SQLModel, table=True):
|
||||
humidite_air: Optional[float] = None
|
||||
humidite_sol: Optional[float] = None
|
||||
source: str = "manuel" # manuel | capteur
|
||||
ts: datetime = Field(default_factory=datetime.utcnow)
|
||||
ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class PlantVariety(SQLModel, table=True):
|
||||
class Plant(SQLModel, table=True):
|
||||
__tablename__ = "plant"
|
||||
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
nom_commun: str
|
||||
nom_botanique: Optional[str] = None
|
||||
@@ -11,9 +13,11 @@ class PlantVariety(SQLModel, table=True):
|
||||
famille: Optional[str] = None
|
||||
tags: Optional[str] = None # CSV
|
||||
type_plante: Optional[str] = None # legume | fruit | aromatique | fleur
|
||||
categorie: Optional[str] = None # potager|fleur|arbre|arbuste
|
||||
besoin_eau: Optional[str] = None # faible | moyen | fort
|
||||
besoin_soleil: Optional[str] = None
|
||||
espacement_cm: Optional[int] = None
|
||||
hauteur_cm: Optional[int] = None
|
||||
temp_min_c: Optional[float] = None
|
||||
duree_culture_j: Optional[int] = None
|
||||
profondeur_semis_cm: Optional[float] = None
|
||||
@@ -23,13 +27,16 @@ class PlantVariety(SQLModel, table=True):
|
||||
repiquage_mois: Optional[str] = None
|
||||
plantation_mois: Optional[str] = None
|
||||
recolte_mois: Optional[str] = None
|
||||
maladies_courantes: Optional[str] = None
|
||||
astuces_culture: Optional[str] = None
|
||||
url_reference: Optional[str] = None
|
||||
notes: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class PlantImage(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
variety_id: int = Field(foreign_key="plantvariety.id", index=True)
|
||||
plant_id: int = Field(foreign_key="plant.id", index=True)
|
||||
filename: str
|
||||
caption: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
@@ -1,12 +1,28 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class PlantingCreate(SQLModel):
|
||||
garden_id: int
|
||||
variety_id: int
|
||||
cell_id: Optional[int] = None
|
||||
date_semis: Optional[date] = None
|
||||
date_plantation: Optional[date] = None
|
||||
date_repiquage: Optional[date] = None
|
||||
quantite: int = 1
|
||||
statut: str = "prevu"
|
||||
date_recolte_debut: Optional[date] = None
|
||||
date_recolte_fin: Optional[date] = None
|
||||
rendement_estime: Optional[float] = None
|
||||
rendement_reel: Optional[float] = None
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class Planting(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
garden_id: int = Field(foreign_key="garden.id", index=True)
|
||||
variety_id: int = Field(foreign_key="plantvariety.id", index=True)
|
||||
variety_id: int = Field(foreign_key="plant.id", index=True)
|
||||
cell_id: Optional[int] = Field(default=None, foreign_key="gardencell.id")
|
||||
date_semis: Optional[date] = None
|
||||
date_plantation: Optional[date] = None
|
||||
@@ -18,8 +34,8 @@ class Planting(SQLModel, table=True):
|
||||
rendement_estime: Optional[float] = None
|
||||
rendement_reel: Optional[float] = None
|
||||
notes: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class PlantingEvent(SQLModel, table=True):
|
||||
@@ -27,4 +43,4 @@ class PlantingEvent(SQLModel, table=True):
|
||||
planting_id: int = Field(foreign_key="planting.id", index=True)
|
||||
type: str # arrosage | taille | traitement | observation | autre
|
||||
note: Optional[str] = None
|
||||
ts: datetime = Field(default_factory=datetime.utcnow)
|
||||
ts: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
42
backend/app/models/recolte.py
Normal file
42
backend/app/models/recolte.py
Normal file
@@ -0,0 +1,42 @@
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class RecolteCreate(SQLModel):
|
||||
quantite: float
|
||||
unite: str = "kg" # kg|g|unites|litres|bottes
|
||||
date_recolte: date
|
||||
notes: Optional[str] = None
|
||||
|
||||
|
||||
class Recolte(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
plantation_id: int = Field(foreign_key="planting.id", index=True)
|
||||
quantite: float
|
||||
unite: str = "kg"
|
||||
date_recolte: date
|
||||
notes: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
|
||||
class ObservationCreate(SQLModel):
|
||||
type: str # maladie|ravageur|traitement|note
|
||||
titre: str
|
||||
description: Optional[str] = None
|
||||
date: date
|
||||
photo_url: Optional[str] = None
|
||||
plantation_id: Optional[int] = None
|
||||
garden_id: Optional[int] = None
|
||||
|
||||
|
||||
class Observation(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
type: str
|
||||
titre: str
|
||||
description: Optional[str] = None
|
||||
date: date
|
||||
photo_url: Optional[str] = None
|
||||
plantation_id: Optional[int] = Field(default=None, foreign_key="planting.id")
|
||||
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
@@ -1,17 +1,34 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import date, datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class TaskCreate(SQLModel):
|
||||
titre: str
|
||||
description: Optional[str] = None
|
||||
garden_id: Optional[int] = None
|
||||
planting_id: Optional[int] = None
|
||||
outil_id: Optional[int] = None
|
||||
priorite: str = "normale" # basse | normale | haute
|
||||
echeance: Optional[date] = None
|
||||
recurrence: Optional[str] = None
|
||||
frequence_jours: Optional[int] = None
|
||||
date_prochaine: Optional[date] = None
|
||||
statut: str = "a_faire" # a_faire | en_cours | fait | annule
|
||||
|
||||
|
||||
class Task(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
titre: str
|
||||
description: Optional[str] = None
|
||||
garden_id: Optional[int] = Field(default=None, foreign_key="garden.id")
|
||||
planting_id: Optional[int] = Field(default=None, foreign_key="planting.id")
|
||||
priorite: str = "normale" # basse | normale | haute
|
||||
outil_id: Optional[int] = Field(default=None, foreign_key="tool.id")
|
||||
priorite: str = "normale"
|
||||
echeance: Optional[date] = None
|
||||
recurrence: Optional[str] = None # quotidien | hebdomadaire | mensuel
|
||||
statut: str = "a_faire" # a_faire | en_cours | fait | annule
|
||||
created_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
||||
recurrence: Optional[str] = None
|
||||
frequence_jours: Optional[int] = None
|
||||
date_prochaine: Optional[date] = None
|
||||
statut: str = "a_faire"
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
|
||||
12
backend/app/models/tool.py
Normal file
12
backend/app/models/tool.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
from sqlmodel import Field, SQLModel
|
||||
|
||||
|
||||
class Tool(SQLModel, table=True):
|
||||
id: Optional[int] = Field(default=None, primary_key=True)
|
||||
nom: str
|
||||
description: Optional[str] = None
|
||||
categorie: Optional[str] = None # beche|fourche|griffe|arrosage|taille|autre
|
||||
photo_url: Optional[str] = None
|
||||
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
59
backend/app/routers/astuces.py
Normal file
59
backend/app/routers/astuces.py
Normal file
@@ -0,0 +1,59 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.astuce import Astuce
|
||||
|
||||
router = APIRouter(tags=["astuces"])
|
||||
|
||||
|
||||
@router.get("/astuces", response_model=List[Astuce])
|
||||
def list_astuces(
|
||||
entity_type: Optional[str] = Query(None),
|
||||
entity_id: Optional[int] = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
q = select(Astuce)
|
||||
if entity_type:
|
||||
q = q.where(Astuce.entity_type == entity_type)
|
||||
if entity_id is not None:
|
||||
q = q.where(Astuce.entity_id == entity_id)
|
||||
return session.exec(q).all()
|
||||
|
||||
|
||||
@router.post("/astuces", response_model=Astuce, status_code=status.HTTP_201_CREATED)
|
||||
def create_astuce(a: Astuce, session: Session = Depends(get_session)):
|
||||
session.add(a)
|
||||
session.commit()
|
||||
session.refresh(a)
|
||||
return a
|
||||
|
||||
|
||||
@router.get("/astuces/{id}", response_model=Astuce)
|
||||
def get_astuce(id: int, session: Session = Depends(get_session)):
|
||||
a = session.get(Astuce, id)
|
||||
if not a:
|
||||
raise HTTPException(404, "Astuce introuvable")
|
||||
return a
|
||||
|
||||
|
||||
@router.put("/astuces/{id}", response_model=Astuce)
|
||||
def update_astuce(id: int, data: Astuce, session: Session = Depends(get_session)):
|
||||
a = session.get(Astuce, id)
|
||||
if not a:
|
||||
raise HTTPException(404, "Astuce introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
setattr(a, k, v)
|
||||
session.add(a)
|
||||
session.commit()
|
||||
session.refresh(a)
|
||||
return a
|
||||
|
||||
|
||||
@router.delete("/astuces/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_astuce(id: int, session: Session = Depends(get_session)):
|
||||
a = session.get(Astuce, id)
|
||||
if not a:
|
||||
raise HTTPException(404, "Astuce introuvable")
|
||||
session.delete(a)
|
||||
session.commit()
|
||||
18
backend/app/routers/dictons.py
Normal file
18
backend/app/routers/dictons.py
Normal file
@@ -0,0 +1,18 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.dicton import Dicton
|
||||
|
||||
router = APIRouter(tags=["dictons"])
|
||||
|
||||
|
||||
@router.get("/dictons", response_model=List[Dicton])
|
||||
def list_dictons(
|
||||
mois: Optional[int] = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
q = select(Dicton)
|
||||
if mois:
|
||||
q = q.where(Dicton.mois == mois)
|
||||
return session.exec(q).all()
|
||||
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
@@ -38,7 +38,7 @@ def update_garden(id: int, data: Garden, session: Session = Depends(get_session)
|
||||
raise HTTPException(status_code=404, detail="Jardin introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
setattr(g, k, v)
|
||||
g.updated_at = datetime.utcnow()
|
||||
g.updated_at = datetime.now(timezone.utc)
|
||||
session.add(g)
|
||||
session.commit()
|
||||
session.refresh(g)
|
||||
|
||||
36
backend/app/routers/lunar.py
Normal file
36
backend/app/routers/lunar.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import calendar
|
||||
from datetime import date
|
||||
from typing import Any
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
|
||||
router = APIRouter(tags=["lunaire"])
|
||||
|
||||
# Cache en mémoire : {mois_str: list[dict]}
|
||||
_CACHE: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
@router.get("/lunar")
|
||||
def get_lunar(
|
||||
month: str = Query(..., description="Format YYYY-MM"),
|
||||
) -> list[dict[str, Any]]:
|
||||
if month in _CACHE:
|
||||
return _CACHE[month]
|
||||
try:
|
||||
year, mon = int(month[:4]), int(month[5:7])
|
||||
except (ValueError, IndexError):
|
||||
raise HTTPException(400, "Format attendu : YYYY-MM")
|
||||
last_day = calendar.monthrange(year, mon)[1]
|
||||
start = date(year, mon, 1)
|
||||
end = date(year, mon, last_day)
|
||||
try:
|
||||
from app.services.lunar import build_calendar
|
||||
from dataclasses import asdict
|
||||
|
||||
result = [asdict(d) for d in build_calendar(start, end)]
|
||||
except ImportError:
|
||||
raise HTTPException(503, "Service lunaire non disponible (skyfield non installé)")
|
||||
except Exception as e:
|
||||
raise HTTPException(500, f"Erreur calcul lunaire : {e}")
|
||||
_CACHE[month] = result
|
||||
return result
|
||||
@@ -2,13 +2,20 @@ import os
|
||||
import uuid
|
||||
from typing import List, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from fastapi import APIRouter, Body, Depends, File, HTTPException, Query, UploadFile, status
|
||||
from pydantic import BaseModel
|
||||
from sqlmodel import Session, select
|
||||
|
||||
from app.config import UPLOAD_DIR
|
||||
from app.database import get_session
|
||||
from app.models.media import Attachment, Media
|
||||
|
||||
|
||||
class MediaPatch(BaseModel):
|
||||
entity_type: Optional[str] = None
|
||||
entity_id: Optional[int] = None
|
||||
titre: Optional[str] = None
|
||||
|
||||
router = APIRouter(tags=["media"])
|
||||
|
||||
|
||||
@@ -81,6 +88,19 @@ def create_media(m: Media, session: Session = Depends(get_session)):
|
||||
return m
|
||||
|
||||
|
||||
@router.patch("/media/{id}", response_model=Media)
|
||||
def update_media(id: int, payload: MediaPatch, session: Session = Depends(get_session)):
|
||||
m = session.get(Media, id)
|
||||
if not m:
|
||||
raise HTTPException(404, "Media introuvable")
|
||||
for k, v in payload.model_dump(exclude_none=True).items():
|
||||
setattr(m, k, v)
|
||||
session.add(m)
|
||||
session.commit()
|
||||
session.refresh(m)
|
||||
return m
|
||||
|
||||
|
||||
@router.delete("/media/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_media(id: int, session: Session = Depends(get_session)):
|
||||
m = session.get(Media, id)
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.planting import Planting, PlantingEvent
|
||||
from app.models.planting import Planting, PlantingCreate, PlantingEvent
|
||||
|
||||
router = APIRouter(tags=["plantations"])
|
||||
|
||||
@@ -14,7 +14,8 @@ def list_plantings(session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.post("/plantings", response_model=Planting, status_code=status.HTTP_201_CREATED)
|
||||
def create_planting(p: Planting, session: Session = Depends(get_session)):
|
||||
def create_planting(data: PlantingCreate, session: Session = Depends(get_session)):
|
||||
p = Planting(**data.model_dump())
|
||||
session.add(p)
|
||||
session.commit()
|
||||
session.refresh(p)
|
||||
@@ -30,13 +31,13 @@ def get_planting(id: int, session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.put("/plantings/{id}", response_model=Planting)
|
||||
def update_planting(id: int, data: Planting, session: Session = Depends(get_session)):
|
||||
def update_planting(id: int, data: PlantingCreate, session: Session = Depends(get_session)):
|
||||
p = session.get(Planting, id)
|
||||
if not p:
|
||||
raise HTTPException(status_code=404, detail="Plantation introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(p, k, v)
|
||||
p.updated_at = datetime.utcnow()
|
||||
p.updated_at = datetime.now(timezone.utc)
|
||||
session.add(p)
|
||||
session.commit()
|
||||
session.refresh(p)
|
||||
|
||||
56
backend/app/routers/plants.py
Normal file
56
backend/app/routers/plants.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.plant import Plant
|
||||
|
||||
router = APIRouter(tags=["plantes"])
|
||||
|
||||
|
||||
@router.get("/plants", response_model=List[Plant])
|
||||
def list_plants(
|
||||
categorie: Optional[str] = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
q = select(Plant)
|
||||
if categorie:
|
||||
q = q.where(Plant.categorie == categorie)
|
||||
return session.exec(q).all()
|
||||
|
||||
|
||||
@router.post("/plants", response_model=Plant, status_code=status.HTTP_201_CREATED)
|
||||
def create_plant(p: Plant, session: Session = Depends(get_session)):
|
||||
session.add(p)
|
||||
session.commit()
|
||||
session.refresh(p)
|
||||
return p
|
||||
|
||||
|
||||
@router.get("/plants/{id}", response_model=Plant)
|
||||
def get_plant(id: int, session: Session = Depends(get_session)):
|
||||
p = session.get(Plant, id)
|
||||
if not p:
|
||||
raise HTTPException(404, "Plante introuvable")
|
||||
return p
|
||||
|
||||
|
||||
@router.put("/plants/{id}", response_model=Plant)
|
||||
def update_plant(id: int, data: Plant, session: Session = Depends(get_session)):
|
||||
p = session.get(Plant, id)
|
||||
if not p:
|
||||
raise HTTPException(404, "Plante introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
setattr(p, k, v)
|
||||
session.add(p)
|
||||
session.commit()
|
||||
session.refresh(p)
|
||||
return p
|
||||
|
||||
|
||||
@router.delete("/plants/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_plant(id: int, session: Session = Depends(get_session)):
|
||||
p = session.get(Plant, id)
|
||||
if not p:
|
||||
raise HTTPException(404, "Plante introuvable")
|
||||
session.delete(p)
|
||||
session.commit()
|
||||
84
backend/app/routers/recoltes.py
Normal file
84
backend/app/routers/recoltes.py
Normal file
@@ -0,0 +1,84 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.recolte import Observation, ObservationCreate, Recolte, RecolteCreate
|
||||
|
||||
router = APIRouter(tags=["récoltes"])
|
||||
|
||||
|
||||
# ── Récoltes (nested sous plantings) ──────────────────────────────────────────
|
||||
|
||||
@router.get("/plantings/{planting_id}/recoltes", response_model=List[Recolte])
|
||||
def list_recoltes(planting_id: int, session: Session = Depends(get_session)):
|
||||
return session.exec(
|
||||
select(Recolte).where(Recolte.plantation_id == planting_id)
|
||||
).all()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/plantings/{planting_id}/recoltes",
|
||||
response_model=Recolte,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
)
|
||||
def create_recolte(
|
||||
planting_id: int,
|
||||
data: RecolteCreate,
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
r = Recolte(plantation_id=planting_id, **data.model_dump())
|
||||
session.add(r)
|
||||
session.commit()
|
||||
session.refresh(r)
|
||||
return r
|
||||
|
||||
|
||||
@router.delete("/recoltes/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_recolte(id: int, session: Session = Depends(get_session)):
|
||||
r = session.get(Recolte, id)
|
||||
if not r:
|
||||
raise HTTPException(404, "Récolte introuvable")
|
||||
session.delete(r)
|
||||
session.commit()
|
||||
|
||||
|
||||
# ── Observations ───────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get("/observations", response_model=List[Observation])
|
||||
def list_observations(
|
||||
plantation_id: Optional[int] = Query(None),
|
||||
garden_id: Optional[int] = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
q = select(Observation)
|
||||
if plantation_id is not None:
|
||||
q = q.where(Observation.plantation_id == plantation_id)
|
||||
if garden_id is not None:
|
||||
q = q.where(Observation.garden_id == garden_id)
|
||||
return session.exec(q).all()
|
||||
|
||||
|
||||
@router.post("/observations", response_model=Observation, status_code=status.HTTP_201_CREATED)
|
||||
def create_observation(data: ObservationCreate, session: Session = Depends(get_session)):
|
||||
o = Observation(**data.model_dump())
|
||||
session.add(o)
|
||||
session.commit()
|
||||
session.refresh(o)
|
||||
return o
|
||||
|
||||
|
||||
@router.get("/observations/{id}", response_model=Observation)
|
||||
def get_observation(id: int, session: Session = Depends(get_session)):
|
||||
o = session.get(Observation, id)
|
||||
if not o:
|
||||
raise HTTPException(404, "Observation introuvable")
|
||||
return o
|
||||
|
||||
|
||||
@router.delete("/observations/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_observation(id: int, session: Session = Depends(get_session)):
|
||||
o = session.get(Observation, id)
|
||||
if not o:
|
||||
raise HTTPException(404, "Observation introuvable")
|
||||
session.delete(o)
|
||||
session.commit()
|
||||
@@ -1,8 +1,7 @@
|
||||
from datetime import date
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.settings import UserSettings, LunarCalendarEntry
|
||||
from app.models.settings import UserSettings
|
||||
|
||||
router = APIRouter(tags=["réglages"])
|
||||
|
||||
@@ -24,16 +23,3 @@ def update_settings(data: dict, session: Session = Depends(get_session)):
|
||||
session.add(row)
|
||||
session.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@router.get("/lunar")
|
||||
def get_lunar(month: str, session: Session = Depends(get_session)):
|
||||
year, m = map(int, month.split("-"))
|
||||
first = date(year, m, 1)
|
||||
last_m, last_y = (m + 1, year) if m < 12 else (1, year + 1)
|
||||
last = date(last_y, last_m, 1)
|
||||
return session.exec(
|
||||
select(LunarCalendarEntry)
|
||||
.where(LunarCalendarEntry.jour >= first)
|
||||
.where(LunarCalendarEntry.jour < last)
|
||||
).all()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.task import Task
|
||||
from app.models.task import Task, TaskCreate
|
||||
|
||||
router = APIRouter(tags=["tâches"])
|
||||
|
||||
@@ -23,7 +23,8 @@ def list_tasks(
|
||||
|
||||
|
||||
@router.post("/tasks", response_model=Task, status_code=status.HTTP_201_CREATED)
|
||||
def create_task(t: Task, session: Session = Depends(get_session)):
|
||||
def create_task(data: TaskCreate, session: Session = Depends(get_session)):
|
||||
t = Task(**data.model_dump())
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.refresh(t)
|
||||
@@ -39,13 +40,26 @@ def get_task(id: int, session: Session = Depends(get_session)):
|
||||
|
||||
|
||||
@router.put("/tasks/{id}", response_model=Task)
|
||||
def update_task(id: int, data: Task, session: Session = Depends(get_session)):
|
||||
def update_task(id: int, data: TaskCreate, session: Session = Depends(get_session)):
|
||||
t = session.get(Task, id)
|
||||
if not t:
|
||||
raise HTTPException(status_code=404, detail="Tâche introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
for k, v in data.model_dump(exclude_unset=True).items():
|
||||
setattr(t, k, v)
|
||||
t.updated_at = datetime.utcnow()
|
||||
t.updated_at = datetime.now(timezone.utc)
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
@router.put("/tasks/{id}/statut", response_model=Task)
|
||||
def update_statut(id: int, statut: str, session: Session = Depends(get_session)):
|
||||
t = session.get(Task, id)
|
||||
if not t:
|
||||
raise HTTPException(status_code=404, detail="Tâche introuvable")
|
||||
t.statut = statut
|
||||
t.updated_at = datetime.now(timezone.utc)
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.refresh(t)
|
||||
|
||||
56
backend/app/routers/tools.py
Normal file
56
backend/app/routers/tools.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlmodel import Session, select
|
||||
from app.database import get_session
|
||||
from app.models.tool import Tool
|
||||
|
||||
router = APIRouter(tags=["outils"])
|
||||
|
||||
|
||||
@router.get("/tools", response_model=List[Tool])
|
||||
def list_tools(
|
||||
categorie: Optional[str] = Query(None),
|
||||
session: Session = Depends(get_session),
|
||||
):
|
||||
q = select(Tool)
|
||||
if categorie:
|
||||
q = q.where(Tool.categorie == categorie)
|
||||
return session.exec(q).all()
|
||||
|
||||
|
||||
@router.post("/tools", response_model=Tool, status_code=status.HTTP_201_CREATED)
|
||||
def create_tool(t: Tool, session: Session = Depends(get_session)):
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
@router.get("/tools/{id}", response_model=Tool)
|
||||
def get_tool(id: int, session: Session = Depends(get_session)):
|
||||
t = session.get(Tool, id)
|
||||
if not t:
|
||||
raise HTTPException(404, "Outil introuvable")
|
||||
return t
|
||||
|
||||
|
||||
@router.put("/tools/{id}", response_model=Tool)
|
||||
def update_tool(id: int, data: Tool, session: Session = Depends(get_session)):
|
||||
t = session.get(Tool, id)
|
||||
if not t:
|
||||
raise HTTPException(404, "Outil introuvable")
|
||||
for k, v in data.model_dump(exclude_unset=True, exclude={"id", "created_at"}).items():
|
||||
setattr(t, k, v)
|
||||
session.add(t)
|
||||
session.commit()
|
||||
session.refresh(t)
|
||||
return t
|
||||
|
||||
|
||||
@router.delete("/tools/{id}", status_code=status.HTTP_204_NO_CONTENT)
|
||||
def delete_tool(id: int, session: Session = Depends(get_session)):
|
||||
t = session.get(Tool, id)
|
||||
if not t:
|
||||
raise HTTPException(404, "Outil introuvable")
|
||||
session.delete(t)
|
||||
session.commit()
|
||||
@@ -6,73 +6,238 @@ import app.models # noqa
|
||||
|
||||
def run_seed():
|
||||
from app.models.garden import Garden, GardenCell, Measurement
|
||||
from app.models.plant import PlantVariety
|
||||
from app.models.plant import Plant
|
||||
from app.models.planting import Planting, PlantingEvent
|
||||
from app.models.task import Task
|
||||
from app.models.tool import Tool
|
||||
from app.models.dicton import Dicton
|
||||
from app.models.astuce import Astuce
|
||||
|
||||
with Session(engine) as session:
|
||||
if session.exec(select(Garden)).first():
|
||||
return # déjà seedé
|
||||
already_seeded = session.exec(select(Garden)).first() is not None
|
||||
|
||||
jardin = Garden(
|
||||
nom="Mon potager",
|
||||
description="Potager principal plein sud",
|
||||
type="plein_air",
|
||||
exposition="S",
|
||||
ombre="plein_soleil",
|
||||
sol_type="limoneux",
|
||||
grille_largeur=6,
|
||||
grille_hauteur=4,
|
||||
)
|
||||
session.add(jardin)
|
||||
session.flush()
|
||||
if not already_seeded:
|
||||
# ── Jardin ────────────────────────────────────────────────────────────
|
||||
jardin = Garden(
|
||||
nom="Mon potager",
|
||||
description="Potager principal plein sud",
|
||||
type="plein_air",
|
||||
exposition="S",
|
||||
ombre="plein_soleil",
|
||||
sol_type="limoneux",
|
||||
surface_m2=24.0,
|
||||
grille_largeur=6,
|
||||
grille_hauteur=4,
|
||||
)
|
||||
session.add(jardin)
|
||||
session.flush()
|
||||
|
||||
for row in range(4):
|
||||
for col in range(6):
|
||||
session.add(GardenCell(
|
||||
garden_id=jardin.id,
|
||||
col=col, row=row,
|
||||
libelle=f"{chr(65 + row)}{col + 1}",
|
||||
))
|
||||
for row in range(4):
|
||||
for col in range(6):
|
||||
session.add(
|
||||
GardenCell(
|
||||
garden_id=jardin.id,
|
||||
col=col,
|
||||
row=row,
|
||||
libelle=f"{chr(65 + row)}{col + 1}",
|
||||
)
|
||||
)
|
||||
|
||||
session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0))
|
||||
session.add(Measurement(garden_id=jardin.id, temp_air=18.0, humidite_air=65.0))
|
||||
|
||||
tomate = PlantVariety(
|
||||
nom_commun="Tomate", variete="Andine Cornue",
|
||||
famille="Solanacées", type_plante="legume",
|
||||
besoin_eau="fort", espacement_cm=60,
|
||||
plantation_mois="4,5", recolte_mois="7,8,9",
|
||||
)
|
||||
courgette = PlantVariety(
|
||||
nom_commun="Courgette", variete="Verte",
|
||||
famille="Cucurbitacées", type_plante="legume",
|
||||
besoin_eau="moyen", espacement_cm=80,
|
||||
plantation_mois="5,6", recolte_mois="7,8",
|
||||
)
|
||||
salade = PlantVariety(
|
||||
nom_commun="Laitue", variete="Batavia",
|
||||
famille="Astéracées", type_plante="legume",
|
||||
besoin_eau="moyen", espacement_cm=25,
|
||||
)
|
||||
session.add_all([tomate, courgette, salade])
|
||||
session.flush()
|
||||
# ── 20 Plantes ────────────────────────────────────────────────────────
|
||||
plantes_data = [
|
||||
dict(nom_commun="Tomate", variete="Andine Cornue", famille="Solanacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="fort",
|
||||
espacement_cm=60, plantation_mois="4,5", recolte_mois="7,8,9",
|
||||
semis_interieur_mois="2,3"),
|
||||
dict(nom_commun="Courgette", variete="Verte", famille="Cucurbitacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=80, plantation_mois="5,6", recolte_mois="7,8",
|
||||
semis_interieur_mois="4"),
|
||||
dict(nom_commun="Carotte", famille="Apiacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=8, semis_exterieur_mois="3,4,5,6",
|
||||
recolte_mois="6,7,8,9,10"),
|
||||
dict(nom_commun="Laitue", variete="Batavia", famille="Astéracées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=25, plantation_mois="3,4,5,8,9",
|
||||
recolte_mois="5,6,7,10"),
|
||||
dict(nom_commun="Ail", famille="Amaryllidacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="faible",
|
||||
espacement_cm=15, plantation_mois="10,11",
|
||||
recolte_mois="6,7"),
|
||||
dict(nom_commun="Oignon", famille="Amaryllidacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="faible",
|
||||
espacement_cm=10, semis_interieur_mois="2,3",
|
||||
plantation_mois="4,5", recolte_mois="7,8"),
|
||||
dict(nom_commun="Haricot", variete="Nain", famille="Fabacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=15, semis_exterieur_mois="5,6",
|
||||
recolte_mois="7,8,9"),
|
||||
dict(nom_commun="Pois", variete="Mange-tout", famille="Fabacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=10, semis_exterieur_mois="3,4",
|
||||
recolte_mois="6,7"),
|
||||
dict(nom_commun="Poireau", famille="Amaryllidacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=15, semis_interieur_mois="2,3",
|
||||
plantation_mois="6,7", recolte_mois="10,11,12,1,2"),
|
||||
dict(nom_commun="Pomme de terre", famille="Solanacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=35, plantation_mois="3,4,5",
|
||||
recolte_mois="7,8,9,10"),
|
||||
dict(nom_commun="Fraise", famille="Rosacées",
|
||||
categorie="potager", type_plante="fruit", besoin_eau="moyen",
|
||||
espacement_cm=30, plantation_mois="3,4,9,10",
|
||||
recolte_mois="5,6,7"),
|
||||
dict(nom_commun="Framboise", famille="Rosacées",
|
||||
categorie="arbuste", type_plante="fruit", besoin_eau="moyen",
|
||||
espacement_cm=60, plantation_mois="11,12,2,3",
|
||||
recolte_mois="7,8,9"),
|
||||
dict(nom_commun="Persil", famille="Apiacées",
|
||||
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
|
||||
espacement_cm=20, semis_exterieur_mois="3,4,5,8",
|
||||
recolte_mois="4,5,6,7,8,9,10"),
|
||||
dict(nom_commun="Échalote", famille="Amaryllidacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="faible",
|
||||
espacement_cm=15, plantation_mois="2,3",
|
||||
recolte_mois="7,8"),
|
||||
dict(nom_commun="Chou-fleur", famille="Brassicacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="fort",
|
||||
espacement_cm=60, semis_interieur_mois="3,4",
|
||||
plantation_mois="5,6", recolte_mois="9,10,11"),
|
||||
dict(nom_commun="Chou", variete="Milan", famille="Brassicacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=50, semis_interieur_mois="3,4",
|
||||
plantation_mois="5,6", recolte_mois="10,11,12"),
|
||||
dict(nom_commun="Betterave", famille="Amaranthacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=15, semis_exterieur_mois="4,5,6",
|
||||
recolte_mois="8,9,10"),
|
||||
dict(nom_commun="Radis", famille="Brassicacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=5, semis_exterieur_mois="3,4,5,8,9",
|
||||
recolte_mois="4,5,6,9,10"),
|
||||
dict(nom_commun="Épinard", famille="Amaranthacées",
|
||||
categorie="potager", type_plante="legume", besoin_eau="moyen",
|
||||
espacement_cm=15, semis_exterieur_mois="3,4,8,9",
|
||||
recolte_mois="5,6,10,11"),
|
||||
dict(nom_commun="Basilic", famille="Lamiacées",
|
||||
categorie="potager", type_plante="aromatique", besoin_eau="moyen",
|
||||
espacement_cm=20, semis_interieur_mois="3,4",
|
||||
plantation_mois="5,6", recolte_mois="6,7,8,9"),
|
||||
]
|
||||
|
||||
p1 = Planting(
|
||||
garden_id=jardin.id, variety_id=tomate.id,
|
||||
date_plantation=date(2026, 5, 1), quantite=6, statut="en_cours",
|
||||
)
|
||||
p2 = Planting(
|
||||
garden_id=jardin.id, variety_id=courgette.id,
|
||||
date_plantation=date(2026, 5, 15), quantite=3, statut="prevu",
|
||||
)
|
||||
session.add_all([p1, p2])
|
||||
session.flush()
|
||||
plantes = []
|
||||
for data in plantes_data:
|
||||
p = Plant(**data)
|
||||
session.add(p)
|
||||
plantes.append(p)
|
||||
session.flush()
|
||||
|
||||
session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin"))
|
||||
tomate = plantes[0]
|
||||
courgette = plantes[1]
|
||||
|
||||
session.add(Task(titre="Arroser les tomates", priorite="haute",
|
||||
statut="a_faire", garden_id=jardin.id))
|
||||
session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire"))
|
||||
session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours"))
|
||||
# ── Plantings ──────────────────────────────────────────────────────────
|
||||
p1 = Planting(
|
||||
garden_id=jardin.id,
|
||||
variety_id=tomate.id,
|
||||
date_plantation=date(2026, 5, 1),
|
||||
quantite=6,
|
||||
statut="en_cours",
|
||||
)
|
||||
p2 = Planting(
|
||||
garden_id=jardin.id,
|
||||
variety_id=courgette.id,
|
||||
date_plantation=date(2026, 5, 15),
|
||||
quantite=3,
|
||||
statut="prevu",
|
||||
)
|
||||
session.add_all([p1, p2])
|
||||
session.flush()
|
||||
session.add(PlantingEvent(planting_id=p1.id, type="arrosage", note="Arrosage du matin"))
|
||||
|
||||
# ── Tâches ────────────────────────────────────────────────────────────
|
||||
session.add(Task(titre="Arroser les tomates", priorite="haute",
|
||||
statut="a_faire", garden_id=jardin.id))
|
||||
session.add(Task(titre="Traiter contre les pucerons", priorite="normale", statut="a_faire"))
|
||||
session.add(Task(titre="Préparer le compost", priorite="basse", statut="en_cours"))
|
||||
|
||||
# ── Outils (indépendant du jardin) ────────────────────────────────────────
|
||||
if not session.exec(select(Tool)).first():
|
||||
outils_data = [
|
||||
dict(nom="Bêche", categorie="beche",
|
||||
description="Bêche acier forgé, manche bois 110 cm"),
|
||||
dict(nom="Fourche-bêche", categorie="fourche",
|
||||
description="Fourche à bêcher 4 dents inox"),
|
||||
dict(nom="Grelinette", categorie="fourche",
|
||||
description="Aérateur bi-fourche ergonomique"),
|
||||
dict(nom="Pioche", categorie="beche",
|
||||
description="Pioche légère pour travaux de surface"),
|
||||
dict(nom="Sarcloir", categorie="griffe",
|
||||
description="Sarcloir oscillant pour désherber entre les rangs"),
|
||||
dict(nom="Râteau", categorie="griffe",
|
||||
description="Râteau métallique 14 dents"),
|
||||
dict(nom="Binette", categorie="griffe",
|
||||
description="Binette pour ameublir et désherber"),
|
||||
dict(nom="Transplantoir", categorie="taille",
|
||||
description="Transplantoir inox gradué"),
|
||||
dict(nom="Arrosoir", categorie="arrosage",
|
||||
description="Arrosoir 10L avec pomme amovible"),
|
||||
dict(nom="Sécateur", categorie="taille",
|
||||
description="Sécateur de précision bypass"),
|
||||
]
|
||||
for data in outils_data:
|
||||
session.add(Tool(**data))
|
||||
|
||||
# ── Dictons (indépendant du jardin) ──────────────────────────────────────
|
||||
if not session.exec(select(Dicton)).first():
|
||||
dictons_data = [
|
||||
dict(mois=1, texte="En janvier, la neige au potager réjouit le jardinier.", region="National"),
|
||||
dict(mois=2, texte="À la Chandeleur, l'hiver reste ou reprend vigueur.", region="National"),
|
||||
dict(mois=3, texte="Mars venteux, avril pluvieux, font mai fleureux.", region="National"),
|
||||
dict(mois=3, texte="Quand mars se déguise en été, avril se déguise en hiver.", region="Auvergne"),
|
||||
dict(mois=4, texte="Avril ne te découvre pas d'un fil.", region="National"),
|
||||
dict(mois=4, texte="Pluie d'avril, fleurs à l'infini.", region="Haute-Loire"),
|
||||
dict(mois=5, texte="Gelées de mai, misère chez le jardinier.", region="Haute-Loire"),
|
||||
dict(mois=5, jour=11, texte="Saints de glace : Mamert, Pancrace et Gervais.", region="National"),
|
||||
dict(mois=6, texte="Juin sec, juillet pluvieux ; juillet sec, grains savoureux.", region="Auvergne"),
|
||||
dict(mois=7, texte="Pluie de juillet remplit greniers et cuves.", region="National"),
|
||||
dict(mois=8, texte="Août chaud, vin bon.", region="Auvergne"),
|
||||
dict(mois=9, texte="En septembre, qui sème du blé en fait son profit.", region="National"),
|
||||
dict(mois=10, texte="En octobre, glands à foison, bon hiver selon la raison.", region="Haute-Loire"),
|
||||
dict(mois=11, texte="À la Saint-Martin, bois ton vin.", region="National"),
|
||||
dict(mois=12, texte="Noël au balcon, Pâques aux tisons.", region="Auvergne"),
|
||||
]
|
||||
for data in dictons_data:
|
||||
session.add(Dicton(**data))
|
||||
|
||||
# ── Astuces (indépendant du jardin) ──────────────────────────────────────
|
||||
if not session.exec(select(Astuce)).first():
|
||||
astuces_data = [
|
||||
dict(titre="Rotation des cultures", entity_type="general",
|
||||
contenu="Changez chaque année la famille de légumes sur chaque parcelle pour éviter l'épuisement du sol et les maladies."),
|
||||
dict(titre="Compagnonnage tomate-basilic", entity_type="plante",
|
||||
contenu="Plantez du basilic au pied des tomates : il éloigne les pucerons et améliore le goût des fruits."),
|
||||
dict(titre="Paillage économise l'eau", entity_type="jardin",
|
||||
contenu="Un paillage de 5 à 10 cm (paille, BRF, tontes) réduit les arrosages de moitié et limite les mauvaises herbes."),
|
||||
dict(titre="Arrosage au pied le matin", entity_type="general",
|
||||
contenu="Arrosez toujours au pied des plantes le matin pour éviter les maladies cryptogamiques et la brûlure des feuilles."),
|
||||
dict(titre="Purin d'ortie maison", entity_type="general",
|
||||
contenu="Faites macérer 1 kg d'orties dans 10 L d'eau pendant 10 jours. Diluez à 10 % et arrosez le sol pour stimuler la croissance."),
|
||||
dict(titre="Buttage des pommes de terre", entity_type="plante",
|
||||
contenu="Buttez régulièrement les pommes de terre quand les fanes atteignent 20 cm pour favoriser la tubérisation."),
|
||||
dict(titre="Semis de carottes en gel", entity_type="plante",
|
||||
contenu="Mélangez les graines de carottes avec du sable fin pour un semis homogène et clairsemé."),
|
||||
dict(titre="Récupération d'eau de pluie", entity_type="jardin",
|
||||
contenu="Installez une cuve de récupération d'eau de pluie : une maison avec 100 m² de toiture collecte 60 000 L/an."),
|
||||
dict(titre="Calendrier lunaire", entity_type="general",
|
||||
contenu="Semez les légumes-feuilles en lune montante, les légumes-racines en lune descendante pour de meilleurs résultats."),
|
||||
dict(titre="Taille en vert des tomates", entity_type="plante",
|
||||
contenu="Pincez les gourmands (tiges secondaires entre tige principale et feuille) pour concentrer l'énergie sur les fruits."),
|
||||
]
|
||||
for data in astuces_data:
|
||||
session.add(Astuce(**data))
|
||||
|
||||
session.commit()
|
||||
|
||||
BIN
backend/jardin.db
Normal file
BIN
backend/jardin.db
Normal file
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
def test_create_planting(client):
|
||||
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
|
||||
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
|
||||
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
|
||||
r = client.post("/api/plantings", json={
|
||||
"garden_id": g["id"], "variety_id": v["id"], "quantite": 3
|
||||
})
|
||||
@@ -10,7 +10,7 @@ def test_create_planting(client):
|
||||
|
||||
def test_list_plantings(client):
|
||||
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
|
||||
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
|
||||
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
|
||||
client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]})
|
||||
r = client.get("/api/plantings")
|
||||
assert r.status_code == 200
|
||||
@@ -19,7 +19,7 @@ def test_list_plantings(client):
|
||||
|
||||
def test_add_planting_event(client):
|
||||
g = client.post("/api/gardens", json={"nom": "Potager", "type": "plein_air"}).json()
|
||||
v = client.post("/api/varieties", json={"nom_commun": "Tomate"}).json()
|
||||
v = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
|
||||
p = client.post("/api/plantings", json={"garden_id": g["id"], "variety_id": v["id"]}).json()
|
||||
r = client.post(f"/api/plantings/{p['id']}/events", json={"type": "arrosage", "note": "Bien arrosé"})
|
||||
assert r.status_code == 201
|
||||
|
||||
26
backend/tests/test_plants.py
Normal file
26
backend/tests/test_plants.py
Normal file
@@ -0,0 +1,26 @@
|
||||
def test_create_plant(client):
|
||||
r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
|
||||
assert r.status_code == 201
|
||||
assert r.json()["nom_commun"] == "Tomate"
|
||||
|
||||
|
||||
def test_list_plants(client):
|
||||
client.post("/api/plants", json={"nom_commun": "Tomate"})
|
||||
client.post("/api/plants", json={"nom_commun": "Courgette"})
|
||||
r = client.get("/api/plants")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 2
|
||||
|
||||
|
||||
def test_get_plant(client):
|
||||
r = client.post("/api/plants", json={"nom_commun": "Basilic"})
|
||||
id = r.json()["id"]
|
||||
r2 = client.get(f"/api/plants/{id}")
|
||||
assert r2.status_code == 200
|
||||
|
||||
|
||||
def test_delete_plant(client):
|
||||
r = client.post("/api/plants", json={"nom_commun": "Test"})
|
||||
id = r.json()["id"]
|
||||
r2 = client.delete(f"/api/plants/{id}")
|
||||
assert r2.status_code == 204
|
||||
36
backend/tests/test_recoltes.py
Normal file
36
backend/tests/test_recoltes.py
Normal file
@@ -0,0 +1,36 @@
|
||||
def test_create_recolte(client):
|
||||
# créer jardin + plante + plantation d'abord
|
||||
g = client.post(
|
||||
"/api/gardens",
|
||||
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
|
||||
).json()
|
||||
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
|
||||
pl = client.post(
|
||||
"/api/plantings",
|
||||
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
|
||||
).json()
|
||||
r = client.post(
|
||||
f"/api/plantings/{pl['id']}/recoltes",
|
||||
json={"quantite": 2.5, "unite": "kg", "date_recolte": "2026-08-01"},
|
||||
)
|
||||
assert r.status_code == 201
|
||||
assert r.json()["quantite"] == 2.5
|
||||
|
||||
|
||||
def test_list_recoltes(client):
|
||||
g = client.post(
|
||||
"/api/gardens",
|
||||
json={"nom": "J", "grille_largeur": 2, "grille_hauteur": 2, "type": "plein_air"},
|
||||
).json()
|
||||
p = client.post("/api/plants", json={"nom_commun": "Tomate"}).json()
|
||||
pl = client.post(
|
||||
"/api/plantings",
|
||||
json={"garden_id": g["id"], "variety_id": p["id"], "quantite": 1, "statut": "en_cours"},
|
||||
).json()
|
||||
client.post(
|
||||
f"/api/plantings/{pl['id']}/recoltes",
|
||||
json={"quantite": 1, "unite": "kg", "date_recolte": "2026-08-01"},
|
||||
)
|
||||
r = client.get(f"/api/plantings/{pl['id']}/recoltes")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 1
|
||||
18
backend/tests/test_tools.py
Normal file
18
backend/tests/test_tools.py
Normal file
@@ -0,0 +1,18 @@
|
||||
def test_create_tool(client):
|
||||
r = client.post("/api/tools", json={"nom": "Bêche", "categorie": "beche"})
|
||||
assert r.status_code == 201
|
||||
assert r.json()["nom"] == "Bêche"
|
||||
|
||||
|
||||
def test_list_tools(client):
|
||||
client.post("/api/tools", json={"nom": "Outil1"})
|
||||
r = client.get("/api/tools")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) >= 1
|
||||
|
||||
|
||||
def test_delete_tool(client):
|
||||
r = client.post("/api/tools", json={"nom": "Test"})
|
||||
id = r.json()["id"]
|
||||
r2 = client.delete(f"/api/tools/{id}")
|
||||
assert r2.status_code == 204
|
||||
@@ -1,26 +1,29 @@
|
||||
"""Tests de l'ancien endpoint /api/varieties — maintenant redirigé vers /api/plants."""
|
||||
|
||||
|
||||
def test_create_variety(client):
|
||||
r = client.post("/api/varieties", json={"nom_commun": "Tomate", "famille": "Solanacées"})
|
||||
r = client.post("/api/plants", json={"nom_commun": "Tomate", "famille": "Solanacées"})
|
||||
assert r.status_code == 201
|
||||
assert r.json()["nom_commun"] == "Tomate"
|
||||
|
||||
|
||||
def test_list_varieties(client):
|
||||
client.post("/api/varieties", json={"nom_commun": "Tomate"})
|
||||
client.post("/api/varieties", json={"nom_commun": "Courgette"})
|
||||
r = client.get("/api/varieties")
|
||||
client.post("/api/plants", json={"nom_commun": "Tomate"})
|
||||
client.post("/api/plants", json={"nom_commun": "Courgette"})
|
||||
r = client.get("/api/plants")
|
||||
assert r.status_code == 200
|
||||
assert len(r.json()) == 2
|
||||
|
||||
|
||||
def test_get_variety(client):
|
||||
r = client.post("/api/varieties", json={"nom_commun": "Basilic"})
|
||||
r = client.post("/api/plants", json={"nom_commun": "Basilic"})
|
||||
id = r.json()["id"]
|
||||
r2 = client.get(f"/api/varieties/{id}")
|
||||
r2 = client.get(f"/api/plants/{id}")
|
||||
assert r2.status_code == 200
|
||||
|
||||
|
||||
def test_delete_variety(client):
|
||||
r = client.post("/api/varieties", json={"nom_commun": "Test"})
|
||||
r = client.post("/api/plants", json={"nom_commun": "Test"})
|
||||
id = r.json()["id"]
|
||||
r2 = client.delete(f"/api/varieties/{id}")
|
||||
r2 = client.delete(f"/api/plants/{id}")
|
||||
assert r2.status_code == 204
|
||||
|
||||
Reference in New Issue
Block a user