Adding AugmentedItem notes

This commit is contained in:
Ian Roddis committed 2026-07-31 09:12:41 -03:00
1 parent 30a13644ce
commit 34679d53e8
10 files changed
+254 -12

No files matched your search

+22
View File
@@ -4,10 +4,13 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.crud import audit as audit_crud
from app.crud import augmented_part as augmented_part_crud
from app.crud import augmented_part_note as augmented_part_note_crud
from app.deps import require_permission
from app.models.user import User
from app.schemas.augmented_part import (
AugmentedPartCreate,
AugmentedPartNoteCreate,
AugmentedPartNoteRead,
AugmentedPartRead,
AugmentedPartSyncResult,
AugmentedPartUpdate,
@@ -132,3 +135,22 @@ async def delete_augmented_part(
"AugmentedPart", augmented_part_id, label
),
)
@router.post(
"/{augmented_part_id}/notes",
response_model=AugmentedPartNoteRead,
status_code=status.HTTP_201_CREATED,
)
async def create_augmented_part_note(
augmented_part_id: int,
payload: AugmentedPartNoteCreate,
db: AsyncSession = Depends(get_db),
actor: User = Depends(require_permission("augmented_parts:write")),
):
ap = await augmented_part_crud.get(db, augmented_part_id)
if ap is None:
raise HTTPException(status_code=404, detail="Augmented part not found")
return await augmented_part_note_crud.create(
db, augmented_part=ap, author=actor, body=payload.body
)
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.augmented_part import AugmentedPart, AugmentedPartNote
from app.models.user import User
async def create(
db: AsyncSession, *, augmented_part: AugmentedPart, author: User, body: str
) -> AugmentedPartNote:
note = AugmentedPartNote(
augmented_part_id=augmented_part.id,
user_id=author.id,
username=author.full_name or author.email,
body=body,
)
db.add(note)
await db.commit()
await db.refresh(note)
return note
+2 -1
View File
@@ -8,7 +8,7 @@ from app.models.associations import (
from app.models.attachment_point import AttachmentPoint
from app.models.attachment_point_name import AttachmentPointName
from app.models.audit_log import AuditLog
from app.models.augmented_part import AugmentedPart
from app.models.augmented_part import AugmentedPart, AugmentedPartNote
from app.models.clamp_connection import ClampConnection
from app.models.fit import Fit, FitLabel, FitNote
from app.models.imported_part import ImportedPart
@@ -32,6 +32,7 @@ __all__ = [
"AttachmentPointName",
"AuditLog",
"AugmentedPart",
"AugmentedPartNote",
"ClampConnection",
"Fit",
"FitLabel",
+30 -1
View File
@@ -1,4 +1,6 @@
from sqlalchemy import ForeignKey, String
from datetime import datetime
from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.core.database import Base
@@ -45,3 +47,30 @@ class AugmentedPart(Base):
scope_model: Mapped[ScopeModel | None] = relationship(lazy="selectin")
part_type: Mapped[PartType] = relationship(lazy="selectin")
sub_part_type: Mapped[SubPartType | None] = relationship(lazy="selectin")
notes: Mapped[list["AugmentedPartNote"]] = relationship(
back_populates="augmented_part",
lazy="selectin",
order_by="AugmentedPartNote.created_at",
cascade="all, delete-orphan",
)
class AugmentedPartNote(Base):
__tablename__ = "augmented_part_notes"
id: Mapped[int] = mapped_column(primary_key=True)
augmented_part_id: Mapped[int] = mapped_column(
ForeignKey("augmented_parts.id", ondelete="CASCADE"), nullable=False
)
user_id: Mapped[int | None] = mapped_column(
ForeignKey("users.id", ondelete="SET NULL"), nullable=True
)
# Denormalized snapshot of who wrote the note (mirrors FitNote.username), so
# the note stays attributable even if the user is later deleted.
username: Mapped[str] = mapped_column(String(255), nullable=False)
body: Mapped[str] = mapped_column(Text, nullable=False)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
augmented_part: Mapped[AugmentedPart] = relationship(back_populates="notes")
+26 -1
View File
@@ -1,4 +1,6 @@
from pydantic import BaseModel, ConfigDict
from datetime import datetime
from pydantic import BaseModel, ConfigDict, field_validator
from app.schemas.manufacturer import ManufacturerRead
from app.schemas.part_colour import PartColourRead
@@ -7,6 +9,28 @@ from app.schemas.scope_model import ScopeModelRead
from app.schemas.sub_part_type import SubPartTypeRead
class AugmentedPartNoteCreate(BaseModel):
body: str
@field_validator("body")
@classmethod
def validate_body(cls, v: str) -> str:
if not v.strip():
raise ValueError("body must not be blank")
return v.strip()
class AugmentedPartNoteRead(BaseModel):
model_config = ConfigDict(from_attributes=True)
id: int
augmented_part_id: int
user_id: int | None
username: str
body: str
created_at: datetime
class AugmentedPartBase(BaseModel):
sku: str | None = None
description: str
@@ -46,3 +70,4 @@ class AugmentedPartRead(AugmentedPartBase):
scope_model: ScopeModelRead | None
part_type: PartTypeRead
sub_part_type: SubPartTypeRead | None
notes: list[AugmentedPartNoteRead] = []
@@ -0,0 +1,22 @@
<div id="ap-{{ augmented_part_id }}-notes" class="ap-inline-section">
<h3>Notes</h3>
<div class="fit-notes">
{% for note in notes %}
<div class="fit-note">
<span class="fit-note-meta">{{ note.username }} · {{ note.created_at.strftime("%Y-%m-%d %H:%M") }}</span>
<p class="fit-note-body">{{ note.body }}</p>
</div>
{% else %}
<p class="muted" style="font-size:0.85rem;margin:0 0 0.5rem">No notes recorded yet.</p>
{% endfor %}
{% if can_write_notes %}
<form hx-post="/admin/augmented-parts/{{ augmented_part_id }}/notes"
hx-target="#ap-{{ augmented_part_id }}-notes" hx-swap="outerHTML"
class="inline-edit fit-note-form">
<input type="text" name="body" placeholder="Add a note…" required>
<button type="submit" class="btn btn-ghost">Add note</button>
</form>
{% endif %}
</div>
</div>
@@ -61,5 +61,9 @@
{% include "admin/_augmented_part_fits.html" %}
</div>
{% endif %}
<div class="card">
{% include "admin/_augmented_part_notes.html" %}
</div>
</main>
{% endblock %}
+44 -1
View File
@@ -19,6 +19,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import get_db
from app.crud import audit as audit_crud
from app.crud import augmented_part as augmented_part_crud
from app.crud import augmented_part_note as augmented_part_note_crud
from app.crud import imported_part as imported_part_crud
from app.crud import imported_part_config as imported_part_config_crud
from app.crud import iso_thread as iso_thread_crud
@@ -1803,6 +1804,9 @@ async def augmented_part_detail(
),
"can_read_fits": can_read_fits,
"can_read_attachment_points": can_read_attachment_points,
"augmented_part_id": augmented_part_id,
"notes": ap.notes,
"can_write_notes": _can_write_augmented_part_notes(user),
}
if can_read_attachment_points:
ctx.update(
@@ -2333,7 +2337,9 @@ async def create_augmented_part_fit(
resource_type="Fit",
resource_id=fit.id,
description=audit_crud.describe_create(
"Fit", fit.id, f"attachment points {ap_a.id}↔{ap_b.id} ({label.value})"
"Fit",
fit.id,
f"attachment points {ap_a.id}↔{ap_b.id} ({label.value})",
),
)
if note.strip():
@@ -2464,6 +2470,43 @@ async def recompute_all_fits(
)
# --- Augmented Part Notes -------------------------------------------------
def _can_write_augmented_part_notes(user: User) -> bool:
return user.is_superuser or "augmented_parts:write" in user.permissions
@router.post(
"/augmented-parts/{augmented_part_id}/notes",
response_class=HTMLResponse,
)
async def create_augmented_part_note(
request: Request,
augmented_part_id: int,
body: str = Form(...),
db: AsyncSession = Depends(get_db),
actor: User = Depends(web_require("augmented_parts:write")),
):
ap = await augmented_part_crud.get(db, augmented_part_id)
if ap is None:
raise HTTPException(status_code=404, detail="Augmented part not found")
if body.strip():
await augmented_part_note_crud.create(
db, augmented_part=ap, author=actor, body=body.strip()
)
await db.refresh(ap, attribute_names=["notes"])
return templates.TemplateResponse(
request,
"admin/_augmented_part_notes.html",
{
"augmented_part_id": augmented_part_id,
"notes": ap.notes,
"can_write_notes": _can_write_augmented_part_notes(actor),
},
)
# --- Part Colours ------------------------------------------------------------