Bugs fixed: - formatUpdatedAt treats naive UTC timestamps as UTC (append Z before parsing) - PersonForm/LocationForm X button now inline with star toggle, matching panel style - LocationForm contact placeholder changed from +44 to +61 QA suggestions actioned: - CategoryAutocomplete: replace blur setTimeout with onPointerDown preventDefault - CategoryFilterBar: replace hardcoded 600px maxWidth with 100vw - Location "other" category shows dash instead of styled badge - Delete dead legacy constants files (people/constants.ts, locations/constants.ts) - EntityTable rows: add tabIndex, Enter/Space keyboard navigation, focus ring - Replace Record<string, unknown> casts with typed keyof accessors - Add email validation (field_validator) to Person and Location schemas Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
63 lines
1.6 KiB
Python
63 lines
1.6 KiB
Python
import re
|
|
from pydantic import BaseModel, ConfigDict, field_validator
|
|
from datetime import datetime
|
|
from typing import Optional, Literal
|
|
|
|
_EMAIL_RE = re.compile(r'^[^@\s]+@[^@\s]+\.[^@\s]+$')
|
|
|
|
|
|
class LocationSearchResult(BaseModel):
|
|
source: Literal["local", "nominatim"]
|
|
location_id: Optional[int] = None
|
|
name: str
|
|
address: str
|
|
|
|
|
|
class LocationCreate(BaseModel):
|
|
name: str
|
|
address: str
|
|
category: str = "other"
|
|
notes: Optional[str] = None
|
|
is_frequent: bool = False
|
|
contact_number: Optional[str] = None
|
|
email: Optional[str] = None
|
|
|
|
@field_validator('email')
|
|
@classmethod
|
|
def validate_email(cls, v: str | None) -> str | None:
|
|
if v and not _EMAIL_RE.match(v):
|
|
raise ValueError('Invalid email address')
|
|
return v
|
|
|
|
|
|
class LocationUpdate(BaseModel):
|
|
name: Optional[str] = None
|
|
address: Optional[str] = None
|
|
category: Optional[str] = None
|
|
notes: Optional[str] = None
|
|
is_frequent: Optional[bool] = None
|
|
contact_number: Optional[str] = None
|
|
email: Optional[str] = None
|
|
|
|
@field_validator('email')
|
|
@classmethod
|
|
def validate_email(cls, v: str | None) -> str | None:
|
|
if v and not _EMAIL_RE.match(v):
|
|
raise ValueError('Invalid email address')
|
|
return v
|
|
|
|
|
|
class LocationResponse(BaseModel):
|
|
id: int
|
|
name: str
|
|
address: str
|
|
category: str
|
|
notes: Optional[str]
|
|
is_frequent: bool
|
|
contact_number: Optional[str]
|
|
email: Optional[str]
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|