feat: Add spool adjustment history tracking (#6)
Track history of all spool weight/length adjustments with timestamps and comments:
Backend:
- Add SpoolAdjustment database model with spool_id, timestamp, type, value, comment
- Add Alembic migration for spool_adjustment table
- Add SpoolAdjustment Pydantic model for API responses
- Update use_weight, use_length, and measure functions to record adjustments
- Add GET /api/v1/spool/{id}/adjustments endpoint for history retrieval
- Add optional comment parameter to use/measure endpoints
Frontend:
- Add ISpoolAdjustment interface to spool model
- Add collapsible adjustment history table to spool show page
- Display timestamp, type (weight/length), amount, and comment
- Add translation keys for history table
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,7 +11,7 @@ from fastapi.responses import JSONResponse
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from spoolman.api.v1.models import Message, Spool, SpoolEvent
|
||||
from spoolman.api.v1.models import Message, Spool, SpoolAdjustment, SpoolEvent
|
||||
from spoolman.database import spool
|
||||
from spoolman.database.database import get_db_session
|
||||
from spoolman.database.utils import SortOrder
|
||||
@@ -105,10 +105,22 @@ class SpoolUpdateParameters(SpoolParameters):
|
||||
class SpoolUseParameters(BaseModel):
|
||||
use_length: Optional[float] = Field(None, description="Length of filament to reduce by, in mm.", examples=[2.2])
|
||||
use_weight: Optional[float] = Field(None, description="Filament weight to reduce by, in g.", examples=[5.3])
|
||||
comment: Optional[str] = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description="Optional comment about this adjustment (e.g., 'printed cover piece').",
|
||||
examples=["Test print"],
|
||||
)
|
||||
|
||||
|
||||
class SpoolMeasureParameters(BaseModel):
|
||||
weight: float = Field(description="Current gross weight of the spool, in g.", examples=[200])
|
||||
comment: Optional[str] = Field(
|
||||
None,
|
||||
max_length=256,
|
||||
description="Optional comment about this adjustment (e.g., 'weighed after print').",
|
||||
examples=["After printing benchy"],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
@@ -513,11 +525,11 @@ async def use( # noqa: ANN201
|
||||
)
|
||||
|
||||
if body.use_weight is not None:
|
||||
db_item = await spool.use_weight(db, spool_id, body.use_weight)
|
||||
db_item = await spool.use_weight(db, spool_id, body.use_weight, comment=body.comment)
|
||||
return Spool.from_db(db_item)
|
||||
|
||||
if body.use_length is not None:
|
||||
db_item = await spool.use_length(db, spool_id, body.use_length)
|
||||
db_item = await spool.use_length(db, spool_id, body.use_length, comment=body.comment)
|
||||
return Spool.from_db(db_item)
|
||||
|
||||
return JSONResponse(
|
||||
@@ -543,7 +555,7 @@ async def measure( # noqa: ANN201
|
||||
body: SpoolMeasureParameters,
|
||||
):
|
||||
try:
|
||||
db_item = await spool.measure(db, spool_id, body.weight)
|
||||
db_item = await spool.measure(db, spool_id, body.weight, comment=body.comment)
|
||||
return Spool.from_db(db_item)
|
||||
except SpoolMeasureError as e:
|
||||
logger.exception("Failed to update spool measurement.")
|
||||
@@ -551,3 +563,32 @@ async def measure( # noqa: ANN201
|
||||
status_code=400,
|
||||
content={"message": e.args[0]},
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{spool_id}/adjustments",
|
||||
name="Get spool adjustment history",
|
||||
description="Get the history of all weight/length adjustments for a spool.",
|
||||
response_model_exclude_none=True,
|
||||
responses={
|
||||
200: {"model": list[SpoolAdjustment]},
|
||||
404: {"model": Message},
|
||||
},
|
||||
)
|
||||
async def get_adjustments(
|
||||
db: Annotated[AsyncSession, Depends(get_db_session)],
|
||||
spool_id: int,
|
||||
limit: Annotated[
|
||||
Optional[int],
|
||||
Query(title="Limit", description="Maximum number of items in the response."),
|
||||
] = None,
|
||||
offset: Annotated[int, Query(title="Offset", description="Offset in the full result set if a limit is set.")] = 0,
|
||||
) -> JSONResponse:
|
||||
db_items, total_count = await spool.get_adjustments(db, spool_id, limit=limit, offset=offset)
|
||||
return JSONResponse(
|
||||
content=jsonable_encoder(
|
||||
[SpoolAdjustment.from_db(db_item) for db_item in db_items],
|
||||
exclude_none=True,
|
||||
),
|
||||
headers={"x-total-count": str(total_count)},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user