Major rework of the citizen-facing page: - Chart + sidebar layout (auth/vote/countdown in right sidebar) - DisplaySettings component (font size, chart density, color palettes) - Adaptive CSS with clamp() throughout, responsive breakpoints at 480/768/1024 - Baseline charts zoomed on first tier for small consumption detail - Marginal price chart with dual Y-axes (foyers left, €/m³ right) - Key metrics banner (5 columns: recettes, palier, prix palier, prix médian, mon prix) - Client-side p0/impacts computation, draggable median price bar - Household dots toggle, vote overlay curves - Auth returns volume_m3, vote captures submitted_at - Cleaned header nav (removed Accueil/Super Admin for public visitors) - Terminology: foyer for bills, électeur for votes - 600m³ added to impact reference volumes - Realistic seed votes (50 votes, 3 profiles) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
233 lines
4.5 KiB
Python
233 lines
4.5 KiB
Python
"""Pydantic schemas for API request/response validation."""
|
|
|
|
from datetime import datetime
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
# ── Auth ──
|
|
|
|
class AdminLogin(BaseModel):
|
|
email: str
|
|
password: str
|
|
|
|
|
|
class CitizenVerify(BaseModel):
|
|
commune_slug: str
|
|
auth_code: str
|
|
|
|
|
|
class Token(BaseModel):
|
|
access_token: str
|
|
token_type: str = "bearer"
|
|
role: str
|
|
commune_slug: str | None = None
|
|
volume_m3: float | None = None
|
|
|
|
|
|
# ── Commune ──
|
|
|
|
class CommuneCreate(BaseModel):
|
|
name: str
|
|
slug: str
|
|
description: str = ""
|
|
|
|
|
|
class CommuneUpdate(BaseModel):
|
|
name: str | None = None
|
|
description: str | None = None
|
|
is_active: bool | None = None
|
|
vote_deadline: datetime | None = None
|
|
|
|
|
|
class CommuneOut(BaseModel):
|
|
id: int
|
|
name: str
|
|
slug: str
|
|
description: str
|
|
is_active: bool
|
|
created_at: datetime
|
|
vote_deadline: datetime | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── TariffParams ──
|
|
|
|
class TariffParamsUpdate(BaseModel):
|
|
abop: float | None = None
|
|
abos: float | None = None
|
|
recettes: float | None = None
|
|
pmax: float | None = None
|
|
vmax: float | None = None
|
|
differentiated_tariff: bool | None = None
|
|
data_year: int | None = None
|
|
|
|
|
|
class TariffParamsOut(BaseModel):
|
|
abop: float
|
|
abos: float
|
|
recettes: float
|
|
pmax: float
|
|
vmax: float
|
|
differentiated_tariff: bool = False
|
|
data_year: int | None = None
|
|
data_imported_at: datetime | None = None
|
|
published_vinf: float | None = None
|
|
published_a: float | None = None
|
|
published_b: float | None = None
|
|
published_c: float | None = None
|
|
published_d: float | None = None
|
|
published_e: float | None = None
|
|
published_p0: float | None = None
|
|
published_at: datetime | None = None
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class PublishCurveRequest(BaseModel):
|
|
vinf: float = Field(ge=0)
|
|
a: float = Field(ge=0, le=1)
|
|
b: float = Field(ge=0, le=1)
|
|
c: float = Field(ge=0, le=1)
|
|
d: float = Field(ge=0, le=1)
|
|
e: float = Field(ge=0, le=1)
|
|
|
|
|
|
# ── Household ──
|
|
|
|
class HouseholdOut(BaseModel):
|
|
id: int
|
|
identifier: str
|
|
status: str
|
|
volume_m3: float
|
|
price_paid_eur: float
|
|
auth_code: str
|
|
has_voted: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class HouseholdStats(BaseModel):
|
|
total: int
|
|
rs_count: int
|
|
rp_count: int
|
|
pro_count: int
|
|
total_volume: float
|
|
avg_volume: float
|
|
median_volume: float
|
|
voted_count: int
|
|
data_year: int | None = None
|
|
data_imported_at: datetime | None = None
|
|
|
|
|
|
class ImportPreview(BaseModel):
|
|
valid_rows: int
|
|
errors: list[str]
|
|
sample: list[dict]
|
|
|
|
|
|
class ImportResult(BaseModel):
|
|
created: int
|
|
errors: list[str]
|
|
|
|
|
|
# ── Tariff Compute ──
|
|
|
|
class TariffComputeRequest(BaseModel):
|
|
commune_slug: str
|
|
vinf: float = Field(ge=0)
|
|
a: float = Field(ge=0, le=1)
|
|
b: float = Field(ge=0, le=1)
|
|
c: float = Field(ge=0, le=1)
|
|
d: float = Field(ge=0, le=1)
|
|
e: float = Field(ge=0, le=1)
|
|
|
|
|
|
class ImpactRowOut(BaseModel):
|
|
volume: float
|
|
old_price: float
|
|
new_price_rp: float
|
|
new_price_rs: float
|
|
|
|
|
|
class TariffComputeResponse(BaseModel):
|
|
p0: float
|
|
curve_volumes: list[float]
|
|
curve_prices_m3: list[float]
|
|
curve_bills_rp: list[float]
|
|
curve_bills_rs: list[float]
|
|
impacts: list[ImpactRowOut]
|
|
|
|
|
|
# ── Vote ──
|
|
|
|
class VoteCreate(BaseModel):
|
|
vinf: float = Field(ge=0)
|
|
a: float = Field(ge=0, le=1)
|
|
b: float = Field(ge=0, le=1)
|
|
c: float = Field(ge=0, le=1)
|
|
d: float = Field(ge=0, le=1)
|
|
e: float = Field(ge=0, le=1)
|
|
|
|
|
|
class VoteOut(BaseModel):
|
|
id: int
|
|
household_id: int
|
|
vinf: float
|
|
a: float
|
|
b: float
|
|
c: float
|
|
d: float
|
|
e: float
|
|
computed_p0: float | None
|
|
submitted_at: datetime
|
|
is_active: bool
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
class MedianOut(BaseModel):
|
|
vinf: float
|
|
a: float
|
|
b: float
|
|
c: float
|
|
d: float
|
|
e: float
|
|
computed_p0: float
|
|
vote_count: int
|
|
|
|
|
|
# ── Admin User ──
|
|
|
|
class AdminUserCreate(BaseModel):
|
|
email: str
|
|
password: str
|
|
full_name: str = ""
|
|
role: str = "commune_admin"
|
|
commune_slugs: list[str] = []
|
|
|
|
|
|
class AdminUserOut(BaseModel):
|
|
id: int
|
|
email: str
|
|
full_name: str
|
|
role: str
|
|
|
|
model_config = {"from_attributes": True}
|
|
|
|
|
|
# ── Content ──
|
|
|
|
class ContentUpdate(BaseModel):
|
|
title: str
|
|
body_markdown: str
|
|
|
|
|
|
class ContentOut(BaseModel):
|
|
slug: str
|
|
title: str
|
|
body_markdown: str
|
|
updated_at: datetime
|
|
|
|
model_config = {"from_attributes": True}
|