mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-01 11:47:46 +02:00
The edit dialog is shared between the projects list and the project detail page and seeds itself from whichever project object it is handed. The list payload never carried tags, due_date or priority, so editing from the list showed a blank tags field -- and, unreported, submitted the dialog's default priority over a stored high/urgent one. The component read those fields through a cast, so the compiler never flagged that they were always absent. Put them on ProjectListResponse and ProjectListItem, drop the casts, and let an explicit null clear tags and due date the way it already clears budget and url -- an emptied field was previously sent as undefined and silently reverted. The template list was missing target_parts_count, which the same dialog edits.
304 lines
8.6 KiB
Python
304 lines
8.6 KiB
Python
from datetime import datetime
|
|
|
|
from pydantic import BaseModel, field_validator
|
|
|
|
|
|
def _validate_project_url(value: str | None) -> str | None:
|
|
"""Reject anything that isn't an http(s) URL — the URL is rendered as a
|
|
clickable `<a href>` so a `javascript:` / `data:` / `file:` value would be
|
|
an XSS vector even with React's default escaping (#1155)."""
|
|
if value is None:
|
|
return value
|
|
trimmed = value.strip()
|
|
if not trimmed:
|
|
return None
|
|
lowered = trimmed.lower()
|
|
if not (lowered.startswith("http://") or lowered.startswith("https://")):
|
|
raise ValueError("url must start with http:// or https://")
|
|
return trimmed
|
|
|
|
|
|
class ProjectCreate(BaseModel):
|
|
"""Schema for creating a new project."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
color: str | None = None
|
|
target_count: int | None = None
|
|
target_parts_count: int | None = None
|
|
notes: str | None = None
|
|
tags: str | None = None
|
|
due_date: datetime | None = None
|
|
priority: str = "normal"
|
|
budget: float | None = None
|
|
parent_id: int | None = None # For sub-projects
|
|
url: str | None = None
|
|
|
|
@field_validator("url")
|
|
@classmethod
|
|
def _check_url(cls, v: str | None) -> str | None:
|
|
return _validate_project_url(v)
|
|
|
|
|
|
class ProjectUpdate(BaseModel):
|
|
"""Schema for updating a project."""
|
|
|
|
name: str | None = None
|
|
description: str | None = None
|
|
color: str | None = None
|
|
status: str | None = None # active, completed, archived
|
|
target_count: int | None = None
|
|
target_parts_count: int | None = None
|
|
notes: str | None = None
|
|
tags: str | None = None
|
|
due_date: datetime | None = None
|
|
priority: str | None = None
|
|
budget: float | None = None
|
|
parent_id: int | None = None
|
|
url: str | None = None
|
|
|
|
@field_validator("url")
|
|
@classmethod
|
|
def _check_url(cls, v: str | None) -> str | None:
|
|
return _validate_project_url(v)
|
|
|
|
|
|
class ProjectStats(BaseModel):
|
|
"""Statistics for a project."""
|
|
|
|
total_archives: int = 0 # Number of archive records
|
|
total_items: int = 0 # Sum of quantities (total items printed)
|
|
completed_prints: int = 0 # Sum of quantities for completed prints
|
|
failed_prints: int = 0 # Sum of quantities for failed prints
|
|
queued_prints: int = 0
|
|
in_progress_prints: int = 0
|
|
total_print_time_hours: float = 0.0
|
|
total_filament_grams: float = 0.0
|
|
progress_percent: float | None = None # Based on target_count (plates)
|
|
parts_progress_percent: float | None = None # Based on target_parts_count
|
|
# Cost tracking (Phase 6)
|
|
estimated_cost: float = 0.0 # Based on filament cost
|
|
total_energy_kwh: float = 0.0
|
|
total_energy_cost: float = 0.0
|
|
remaining_prints: int | None = None # target_count - total_archives
|
|
remaining_parts: int | None = None # target_parts_count - completed_prints
|
|
# BOM stats (Phase 7)
|
|
bom_total_items: int = 0
|
|
bom_completed_items: int = 0
|
|
bom_cost: float = 0.0 # Total cost of BOM items (sum of unit_price * quantity_needed)
|
|
|
|
|
|
class ProjectChildPreview(BaseModel):
|
|
"""Minimal project data for child preview."""
|
|
|
|
id: int
|
|
name: str
|
|
color: str | None
|
|
status: str
|
|
progress_percent: float | None = None
|
|
|
|
|
|
class ProjectResponse(BaseModel):
|
|
"""Schema for project response."""
|
|
|
|
id: int
|
|
name: str
|
|
description: str | None
|
|
color: str | None
|
|
status: str
|
|
target_count: int | None
|
|
target_parts_count: int | None = None
|
|
notes: str | None = None
|
|
attachments: list | None = None
|
|
tags: str | None = None
|
|
due_date: datetime | None = None
|
|
priority: str = "normal"
|
|
budget: float | None = None
|
|
is_template: bool = False
|
|
template_source_id: int | None = None
|
|
parent_id: int | None = None
|
|
parent_name: str | None = None # For display
|
|
children: list[ProjectChildPreview] = []
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
stats: ProjectStats | None = None
|
|
url: str | None = None
|
|
cover_image_filename: str | None = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class ArchivePreview(BaseModel):
|
|
"""Minimal archive data for project preview."""
|
|
|
|
id: int
|
|
print_name: str | None
|
|
thumbnail_path: str | None
|
|
status: str
|
|
filament_type: str | None = None
|
|
filament_color: str | None = None
|
|
|
|
|
|
class ProjectListResponse(BaseModel):
|
|
"""Schema for project list item (lighter weight)."""
|
|
|
|
id: int
|
|
name: str
|
|
description: str | None
|
|
color: str | None
|
|
status: str
|
|
target_count: int | None
|
|
target_parts_count: int | None = None
|
|
budget: float | None = None
|
|
# The edit dialog is shared with the project detail page and seeds its fields
|
|
# from whichever project object it is handed, so the list payload has to carry
|
|
# everything the dialog edits — otherwise a save from the list view submits a
|
|
# blank tags field and a default priority over the stored values (#2536).
|
|
tags: str | None = None
|
|
due_date: datetime | None = None
|
|
priority: str = "normal"
|
|
created_at: datetime
|
|
# Quick stats
|
|
archive_count: int = 0 # Number of print jobs
|
|
total_items: int = 0 # Sum of quantities (total items printed, including failed)
|
|
completed_count: int = 0 # Sum of quantities for completed prints only
|
|
failed_count: int = 0 # Sum of quantities for failed prints
|
|
queue_count: int = 0
|
|
progress_percent: float | None = None
|
|
# Preview of archives (up to 5)
|
|
archives: list[ArchivePreview] = []
|
|
# #1155: card-level metadata
|
|
url: str | None = None
|
|
cover_image_filename: str | None = None
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
class BatchAddArchives(BaseModel):
|
|
"""Schema for batch adding archives to a project."""
|
|
|
|
archive_ids: list[int]
|
|
|
|
|
|
class BatchAddQueueItems(BaseModel):
|
|
"""Schema for batch adding queue items to a project."""
|
|
|
|
queue_item_ids: list[int]
|
|
|
|
|
|
# Phase 7: BOM Schemas - Tracks sourced/purchased parts
|
|
class BOMItemCreate(BaseModel):
|
|
"""Schema for creating a BOM item."""
|
|
|
|
name: str
|
|
quantity_needed: int = 1
|
|
unit_price: float | None = None
|
|
sourcing_url: str | None = None
|
|
archive_id: int | None = None
|
|
stl_filename: str | None = None
|
|
remarks: str | None = None
|
|
|
|
|
|
class BOMItemUpdate(BaseModel):
|
|
"""Schema for updating a BOM item."""
|
|
|
|
name: str | None = None
|
|
quantity_needed: int | None = None
|
|
quantity_acquired: int | None = None
|
|
unit_price: float | None = None
|
|
sourcing_url: str | None = None
|
|
archive_id: int | None = None
|
|
stl_filename: str | None = None
|
|
remarks: str | None = None
|
|
|
|
|
|
class BOMItemResponse(BaseModel):
|
|
"""Schema for BOM item response."""
|
|
|
|
id: int
|
|
project_id: int
|
|
name: str
|
|
quantity_needed: int
|
|
quantity_acquired: int
|
|
unit_price: float | None
|
|
sourcing_url: str | None
|
|
archive_id: int | None
|
|
archive_name: str | None = None
|
|
stl_filename: str | None
|
|
remarks: str | None
|
|
sort_order: int
|
|
is_complete: bool = False
|
|
created_at: datetime
|
|
updated_at: datetime
|
|
|
|
class Config:
|
|
from_attributes = True
|
|
|
|
|
|
# Phase 9: Timeline Schemas
|
|
class TimelineEvent(BaseModel):
|
|
"""Schema for a timeline event."""
|
|
|
|
event_type: str # archive_added, queue_started, queue_completed, status_changed, note_updated
|
|
timestamp: datetime
|
|
title: str
|
|
description: str | None = None
|
|
metadata: dict | None = None # Additional event-specific data
|
|
|
|
|
|
# Phase 10: Import/Export Schemas
|
|
class BOMItemExport(BaseModel):
|
|
"""Schema for exporting a BOM item."""
|
|
|
|
name: str
|
|
quantity_needed: int
|
|
quantity_acquired: int
|
|
unit_price: float | None
|
|
sourcing_url: str | None
|
|
stl_filename: str | None
|
|
remarks: str | None
|
|
|
|
|
|
class LinkedFolderExport(BaseModel):
|
|
"""Schema for exporting a linked library folder."""
|
|
|
|
name: str
|
|
|
|
|
|
class ProjectExport(BaseModel):
|
|
"""Schema for exporting a project."""
|
|
|
|
name: str
|
|
description: str | None
|
|
color: str | None
|
|
status: str
|
|
target_count: int | None
|
|
target_parts_count: int | None
|
|
notes: str | None
|
|
tags: str | None
|
|
due_date: datetime | None
|
|
priority: str
|
|
budget: float | None
|
|
bom_items: list[BOMItemExport] = []
|
|
linked_folders: list[LinkedFolderExport] = []
|
|
|
|
|
|
class ProjectImport(BaseModel):
|
|
"""Schema for importing a project."""
|
|
|
|
name: str
|
|
description: str | None = None
|
|
color: str | None = None
|
|
status: str = "active"
|
|
target_count: int | None = None
|
|
target_parts_count: int | None = None
|
|
notes: str | None = None
|
|
tags: str | None = None
|
|
due_date: datetime | None = None
|
|
priority: str = "normal"
|
|
budget: float | None = None
|
|
bom_items: list[BOMItemExport] = []
|
|
linked_folders: list[LinkedFolderExport] = []
|