Fix entry edit always failing with 'Input should be None'
Two bugs: 1. Python 3.12 name collision in schemas.py: `date: Optional[date] = None` caused get_type_hints() to resolve the `date` type annotation to NoneType (Optional[None]) because the field name shadowed the datetime.date import. All *Update schemas were rejecting any PUT with a valid date. Fixed by aliasing the import: `from datetime import date as Date`. 2. FastAPI validation errors return detail as a list of objects, not a string. Passing that list to new Error() produced "Error: [object Object]". Fixed in api.js to map the detail array to msg strings before throwing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
from datetime import date, datetime
|
from datetime import date as Date, datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
@@ -56,18 +56,18 @@ class UserOut(BaseModel):
|
|||||||
# ── Egg Collections ───────────────────────────────────────────────────────────
|
# ── Egg Collections ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class EggCollectionCreate(BaseModel):
|
class EggCollectionCreate(BaseModel):
|
||||||
date: date
|
date: Date
|
||||||
eggs: int = Field(ge=0)
|
eggs: int = Field(ge=0)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class EggCollectionUpdate(BaseModel):
|
class EggCollectionUpdate(BaseModel):
|
||||||
date: Optional[date] = None
|
date: Optional[Date] = None
|
||||||
eggs: Optional[int] = Field(default=None, ge=0)
|
eggs: Optional[int] = Field(default=None, ge=0)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class EggCollectionOut(BaseModel):
|
class EggCollectionOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
date: date
|
date: Date
|
||||||
eggs: int
|
eggs: int
|
||||||
notes: Optional[str]
|
notes: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -78,18 +78,18 @@ class EggCollectionOut(BaseModel):
|
|||||||
# ── Flock History ─────────────────────────────────────────────────────────────
|
# ── Flock History ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class FlockHistoryCreate(BaseModel):
|
class FlockHistoryCreate(BaseModel):
|
||||||
date: date
|
date: Date
|
||||||
chicken_count: int = Field(ge=0)
|
chicken_count: int = Field(ge=0)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class FlockHistoryUpdate(BaseModel):
|
class FlockHistoryUpdate(BaseModel):
|
||||||
date: Optional[date] = None
|
date: Optional[Date] = None
|
||||||
chicken_count: Optional[int] = Field(default=None, ge=0)
|
chicken_count: Optional[int] = Field(default=None, ge=0)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class FlockHistoryOut(BaseModel):
|
class FlockHistoryOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
date: date
|
date: Date
|
||||||
chicken_count: int
|
chicken_count: int
|
||||||
notes: Optional[str]
|
notes: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
@@ -100,20 +100,20 @@ class FlockHistoryOut(BaseModel):
|
|||||||
# ── Feed Purchases ────────────────────────────────────────────────────────────
|
# ── Feed Purchases ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class FeedPurchaseCreate(BaseModel):
|
class FeedPurchaseCreate(BaseModel):
|
||||||
date: date
|
date: Date
|
||||||
bags: Decimal = Field(gt=0, decimal_places=2)
|
bags: Decimal = Field(gt=0, decimal_places=2)
|
||||||
price_per_bag: Decimal = Field(gt=0, decimal_places=2)
|
price_per_bag: Decimal = Field(gt=0, decimal_places=2)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class FeedPurchaseUpdate(BaseModel):
|
class FeedPurchaseUpdate(BaseModel):
|
||||||
date: Optional[date] = None
|
date: Optional[Date] = None
|
||||||
bags: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
bags: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
||||||
price_per_bag: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
price_per_bag: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class FeedPurchaseOut(BaseModel):
|
class FeedPurchaseOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
date: date
|
date: Date
|
||||||
bags: Decimal
|
bags: Decimal
|
||||||
price_per_bag: Decimal
|
price_per_bag: Decimal
|
||||||
notes: Optional[str]
|
notes: Optional[str]
|
||||||
@@ -125,18 +125,18 @@ class FeedPurchaseOut(BaseModel):
|
|||||||
# ── Other Purchases ───────────────────────────────────────────────────────────
|
# ── Other Purchases ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
class OtherPurchaseCreate(BaseModel):
|
class OtherPurchaseCreate(BaseModel):
|
||||||
date: date
|
date: Date
|
||||||
total: Decimal = Field(gt=0, decimal_places=2)
|
total: Decimal = Field(gt=0, decimal_places=2)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class OtherPurchaseUpdate(BaseModel):
|
class OtherPurchaseUpdate(BaseModel):
|
||||||
date: Optional[date] = None
|
date: Optional[Date] = None
|
||||||
total: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
total: Optional[Decimal] = Field(default=None, gt=0, decimal_places=2)
|
||||||
notes: Optional[str] = None
|
notes: Optional[str] = None
|
||||||
|
|
||||||
class OtherPurchaseOut(BaseModel):
|
class OtherPurchaseOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
date: date
|
date: Date
|
||||||
total: Decimal
|
total: Decimal
|
||||||
notes: Optional[str]
|
notes: Optional[str]
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ const API = {
|
|||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
const err = await res.json().catch(() => ({ detail: res.statusText }));
|
||||||
throw new Error(err.detail || `Request failed (${res.status})`);
|
const detail = Array.isArray(err.detail)
|
||||||
|
? err.detail.map(e => e.msg).join(', ')
|
||||||
|
: err.detail;
|
||||||
|
throw new Error(detail || `Request failed (${res.status})`);
|
||||||
}
|
}
|
||||||
if (res.status === 204) return null; // DELETE returns No Content
|
if (res.status === 204) return null; // DELETE returns No Content
|
||||||
return res.json();
|
return res.json();
|
||||||
|
|||||||
Reference in New Issue
Block a user