feat(todos): domains[], photo_path, gps_lat/lng — modèle, schemas, API, tri par date

- Modèle SQLAlchemy : ajout de domains (ARRAY), photo_path, gps_lat, gps_lng ; import Float
- Schemas Pydantic : domain → domains dans TodoCreate, TodoUpdate, TodoResponse ; ajout photo_path, gps_lat, gps_lng
- API GET /api/todos : filtre domain (param URL) redirigé vers domains.contains([domain]) sur le champ ARRAY
- Tests : domain → domains dans les payloads POST ; assertion domains == ["informatique"] dans test_creer_todo
This commit is contained in:
2026-05-24 16:04:21 +02:00
parent a97894437a
commit e9dfb6e293
4 changed files with 22 additions and 8 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ async def list_todos(
): ):
conditions = [] conditions = []
if domain: if domain:
conditions.append(TodoItem.domain == domain) conditions.append(TodoItem.domains.contains([domain]))
if status: if status:
conditions.append(TodoItem.status == status) conditions.append(TodoItem.status == status)
if priority: if priority:
+5 -1
View File
@@ -1,6 +1,6 @@
import uuid import uuid
from datetime import datetime from datetime import datetime
from sqlalchemy import String, Text, Integer, TIMESTAMP, text from sqlalchemy import String, Text, Integer, Float, TIMESTAMP, text
from sqlalchemy.dialects.postgresql import UUID, ARRAY from sqlalchemy.dialects.postgresql import UUID, ARRAY
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
from app.core.database import Base from app.core.database import Base
@@ -15,8 +15,12 @@ class TodoItem(Base):
body: Mapped[str | None] = mapped_column(Text) body: Mapped[str | None] = mapped_column(Text)
url: Mapped[str | None] = mapped_column(Text) url: Mapped[str | None] = mapped_column(Text)
domain: Mapped[str | None] = mapped_column(String(50)) domain: Mapped[str | None] = mapped_column(String(50))
domains: Mapped[list[str]] = mapped_column(ARRAY(String(50)), server_default=text("'{}'::varchar[]"))
category: Mapped[str | None] = mapped_column(String(50)) category: Mapped[str | None] = mapped_column(String(50))
tags: Mapped[list[str]] = mapped_column(ARRAY(String(50)), server_default=text("'{}'::varchar[]")) tags: Mapped[list[str]] = mapped_column(ARRAY(String(50)), server_default=text("'{}'::varchar[]"))
photo_path: Mapped[str | None] = mapped_column(Text)
gps_lat: Mapped[float | None] = mapped_column(Float)
gps_lng: Mapped[float | None] = mapped_column(Float)
status: Mapped[str] = mapped_column(String(20), server_default="pending") status: Mapped[str] = mapped_column(String(20), server_default="pending")
priority: Mapped[str] = mapped_column(String(10), server_default="medium") priority: Mapped[str] = mapped_column(String(10), server_default="medium")
due_date: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True)) due_date: Mapped[datetime | None] = mapped_column(TIMESTAMP(timezone=True))
+12 -3
View File
@@ -8,24 +8,30 @@ class TodoCreate(BaseModel):
title: str title: str
body: str | None = None body: str | None = None
url: str | None = None url: str | None = None
domain: str | None = None domains: list[str] = Field(default_factory=list)
category: str | None = None category: str | None = None
tags: list[str] = Field(default_factory=list) tags: list[str] = Field(default_factory=list)
status: Literal["pending", "done", "cancelled"] = "pending" status: Literal["pending", "done", "cancelled"] = "pending"
priority: Literal["low", "medium", "high"] = "medium" priority: Literal["low", "medium", "high"] = "medium"
due_date: datetime | None = None due_date: datetime | None = None
photo_path: str | None = None
gps_lat: float | None = None
gps_lng: float | None = None
class TodoUpdate(BaseModel): class TodoUpdate(BaseModel):
title: str | None = None title: str | None = None
body: str | None = None body: str | None = None
url: str | None = None url: str | None = None
domain: str | None = None domains: list[str] | None = None
category: str | None = None category: str | None = None
tags: list[str] | None = None tags: list[str] | None = None
status: Literal["pending", "done", "cancelled"] | None = None status: Literal["pending", "done", "cancelled"] | None = None
priority: Literal["low", "medium", "high"] | None = None priority: Literal["low", "medium", "high"] | None = None
due_date: datetime | None = None due_date: datetime | None = None
photo_path: str | None = None
gps_lat: float | None = None
gps_lng: float | None = None
class PostponeRequest(BaseModel): class PostponeRequest(BaseModel):
@@ -39,7 +45,7 @@ class TodoResponse(BaseModel):
title: str title: str
body: str | None body: str | None
url: str | None url: str | None
domain: str | None domains: list[str]
category: str | None category: str | None
tags: list[str] tags: list[str]
status: str status: str
@@ -49,3 +55,6 @@ class TodoResponse(BaseModel):
created_at: datetime created_at: datetime
updated_at: datetime | None updated_at: datetime | None
owner_id: uuid.UUID | None owner_id: uuid.UUID | None
photo_path: str | None
gps_lat: float | None
gps_lng: float | None
+4 -3
View File
@@ -13,7 +13,7 @@ async def cleanup(db_session):
async def test_creer_todo(client): async def test_creer_todo(client):
resp = await client.post("/api/todos/", json={ resp = await client.post("/api/todos/", json={
"title": "TEST_tâche simple", "title": "TEST_tâche simple",
"domain": "informatique", "domains": ["informatique"],
"priority": "high", "priority": "high",
}) })
assert resp.status_code == 201 assert resp.status_code == 201
@@ -22,6 +22,7 @@ async def test_creer_todo(client):
assert data["status"] == "pending" assert data["status"] == "pending"
assert data["postponed_count"] == 0 assert data["postponed_count"] == 0
assert data["tags"] == [] assert data["tags"] == []
assert data["domains"] == ["informatique"]
async def test_lister_todos_filtre_status(client): async def test_lister_todos_filtre_status(client):
@@ -36,8 +37,8 @@ async def test_lister_todos_filtre_status(client):
async def test_lister_todos_filtre_domaine(client): async def test_lister_todos_filtre_domaine(client):
await client.post("/api/todos/", json={"title": "TEST_info", "domain": "informatique"}) await client.post("/api/todos/", json={"title": "TEST_info", "domains": ["informatique"]})
await client.post("/api/todos/", json={"title": "TEST_jardin", "domain": "jardin"}) await client.post("/api/todos/", json={"title": "TEST_jardin", "domains": ["jardin"]})
resp = await client.get("/api/todos/?domain=informatique&status=") resp = await client.get("/api/todos/?domain=informatique&status=")
assert resp.status_code == 200 assert resp.status_code == 200