Adding archived flag to augmented parts, fixing migrations so that creation from scratch is successful
This commit is contained in:
1 parent
740dfc97c7
commit
413e9e8381
27 files changed
+695
-126
No files matched your search
@@ -24,10 +24,13 @@ router = APIRouter()
|
||||
async def list_augmented_parts(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
include_archived: bool = False,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
_: User = Depends(require_permission("augmented_parts:read")),
|
||||
):
|
||||
return await augmented_part_crud.list_all(db, skip=skip, limit=limit)
|
||||
return await augmented_part_crud.list_all(
|
||||
db, skip=skip, limit=limit, include_archived=include_archived
|
||||
)
|
||||
|
||||
|
||||
@router.post("", response_model=AugmentedPartRead, status_code=status.HTTP_201_CREATED)
|
||||
@@ -72,7 +75,7 @@ async def sync_augmented_parts(
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Sync: {result.created} created, {result.deleted} deleted, "
|
||||
f"Sync: {result.created} created, {result.archived} archived, "
|
||||
f"{result.skipped} skipped (no type match)"
|
||||
),
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ from app.schemas.imported_part import (
|
||||
ImportedPartRead,
|
||||
SyncResult,
|
||||
)
|
||||
from app.services import augmented_part_sync
|
||||
from app.services import odata as odata_service
|
||||
|
||||
router = APIRouter()
|
||||
@@ -128,7 +129,9 @@ async def delete_imported_part(
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="Imported part not found"
|
||||
)
|
||||
await imported_part_crud.delete(db, part)
|
||||
archived = await augmented_part_sync.delete_imported_part_and_archive_orphans(
|
||||
db, part
|
||||
)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
@@ -137,3 +140,15 @@ async def delete_imported_part(
|
||||
resource_id=None,
|
||||
description=audit_crud.describe_delete("ImportedPart", 0, inventory_id),
|
||||
)
|
||||
if archived:
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Archived {archived} augmented part(s) left with no remaining "
|
||||
f"imported parts after deleting {inventory_id!r}"
|
||||
),
|
||||
)
|
||||
@@ -58,14 +58,15 @@ async def get_by_sku(db: AsyncSession, sku: str) -> AugmentedPart | None:
|
||||
|
||||
|
||||
async def list_all(
|
||||
db: AsyncSession, skip: int = 0, limit: int = 100
|
||||
db: AsyncSession,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
include_archived: bool = False,
|
||||
) -> list[AugmentedPart]:
|
||||
result = await db.execute(
|
||||
select(AugmentedPart)
|
||||
.order_by(AugmentedPart.description)
|
||||
.offset(skip)
|
||||
.limit(limit)
|
||||
)
|
||||
q = select(AugmentedPart).order_by(AugmentedPart.description)
|
||||
if not include_archived:
|
||||
q = q.where(AugmentedPart.archived.is_(False))
|
||||
result = await db.execute(q.offset(skip).limit(limit))
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
@@ -85,6 +86,7 @@ async def list_filtered(
|
||||
scope_model_id: int | None = None,
|
||||
part_type_id: int | None = None,
|
||||
sub_part_type_id: int | None = None,
|
||||
include_archived: bool = False,
|
||||
sort: str = "description",
|
||||
direction: str = "asc",
|
||||
skip: int = 0,
|
||||
@@ -106,6 +108,8 @@ async def list_filtered(
|
||||
q = q.where(AugmentedPart.part_type_id == part_type_id)
|
||||
if sub_part_type_id is not None:
|
||||
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
||||
if not include_archived:
|
||||
q = q.where(AugmentedPart.archived.is_(False))
|
||||
q = _apply_sort(q, sort, direction)
|
||||
result = await db.execute(q.offset(skip).limit(limit))
|
||||
return list(result.scalars().all())
|
||||
@@ -119,6 +123,7 @@ async def count_filtered(
|
||||
scope_model_id: int | None = None,
|
||||
part_type_id: int | None = None,
|
||||
sub_part_type_id: int | None = None,
|
||||
include_archived: bool = False,
|
||||
) -> int:
|
||||
from sqlalchemy import func
|
||||
|
||||
@@ -138,6 +143,8 @@ async def count_filtered(
|
||||
q = q.where(AugmentedPart.part_type_id == part_type_id)
|
||||
if sub_part_type_id is not None:
|
||||
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
||||
if not include_archived:
|
||||
q = q.where(AugmentedPart.archived.is_(False))
|
||||
result = await db.execute(q)
|
||||
return result.scalar_one()
|
||||
|
||||
@@ -251,6 +258,15 @@ async def get_unlinked_imported_parts(db: AsyncSession) -> list[ImportedPart]:
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def set_archived(
|
||||
db: AsyncSession, ap: AugmentedPart, archived: bool
|
||||
) -> AugmentedPart:
|
||||
ap.archived = archived
|
||||
await db.commit()
|
||||
await db.refresh(ap, attribute_names=_RELATIONSHIPS)
|
||||
return ap
|
||||
|
||||
|
||||
async def delete(db: AsyncSession, ap: AugmentedPart) -> None:
|
||||
await db.delete(ap)
|
||||
await db.commit()
|
||||
@@ -105,11 +105,6 @@ async def count_filtered(db: AsyncSession, description: str = "") -> int:
|
||||
return result.scalar_one()
|
||||
|
||||
|
||||
async def delete(db: AsyncSession, part: ImportedPart) -> None:
|
||||
await db.delete(part)
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def upsert_bulk(db: AsyncSession, records: list[dict]) -> tuple[int, int]:
|
||||
"""Insert-or-update records keyed on InventoryID. Returns (created, updated)."""
|
||||
if not records:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy import Boolean, DateTime, ForeignKey, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.database import Base
|
||||
@@ -36,6 +36,10 @@ class AugmentedPart(Base):
|
||||
nullable=True,
|
||||
default=None,
|
||||
)
|
||||
# Set instead of deleting the row when the last backing ImportedPart is
|
||||
# removed, so fit/attachment-point history is preserved. Hidden from list
|
||||
# views by default.
|
||||
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||
|
||||
# linked imported parts — not eagerly loaded; used by sync service only
|
||||
imported_parts: Mapped[list[ImportedPart]] = relationship(
|
||||
|
||||
@@ -53,11 +53,12 @@ class AugmentedPartUpdate(BaseModel):
|
||||
scope_model_id: int | None = None
|
||||
part_type_id: int | None = None
|
||||
sub_part_type_id: int | None = None
|
||||
archived: bool | None = None
|
||||
|
||||
|
||||
class AugmentedPartSyncResult(BaseModel):
|
||||
created: int
|
||||
deleted: int
|
||||
archived: int
|
||||
skipped: int
|
||||
|
||||
|
||||
@@ -65,6 +66,7 @@ class AugmentedPartRead(AugmentedPartBase):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
id: int
|
||||
archived: bool
|
||||
colour: PartColourRead
|
||||
manufacturer: ManufacturerRead
|
||||
scope_model: ScopeModelRead | None
|
||||
|
||||
@@ -2,11 +2,12 @@
|
||||
|
||||
Algorithm
|
||||
---------
|
||||
Phase 1 – prune stale entries
|
||||
Phase 1 – archive stale entries
|
||||
• Find association rows whose imported_part_id no longer exists.
|
||||
• Delete those rows.
|
||||
• Delete any AugmentedPart that ends up with zero remaining associations
|
||||
(i.e. it was created purely from now-deleted ImportedParts).
|
||||
• Archive (rather than delete) any AugmentedPart that ends up with zero
|
||||
remaining associations (i.e. it was created purely from now-deleted
|
||||
ImportedParts), preserving its fit/attachment-point history.
|
||||
|
||||
Phase 2 – create entries for new ImportedParts
|
||||
• Any ImportedPart not yet linked to an AugmentedPart gets one created.
|
||||
@@ -33,12 +34,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.crud import attachment_point as attachment_point_crud
|
||||
from app.crud import attachment_point_name as attachment_point_name_crud
|
||||
from app.crud import clamp_connection as clamp_connection_crud
|
||||
from app.crud import iso_thread as iso_thread_crud
|
||||
from app.crud import manufacturer as manufacturer_crud
|
||||
from app.crud import part_colour as part_colour_crud
|
||||
from app.crud import part_type as part_type_crud
|
||||
from app.crud import static_connection as static_connection_crud
|
||||
from app.crud import clamp_connection as clamp_connection_crud
|
||||
from app.crud import unc_thread as unc_thread_crud
|
||||
from app.models.associations import augmented_part_imported_parts
|
||||
from app.models.augmented_part import AugmentedPart
|
||||
@@ -268,6 +269,91 @@ def _infer_part_type(description: str, part_types: list[PartType]) -> PartType |
|
||||
return None
|
||||
|
||||
|
||||
async def _archive_orphans(db: AsyncSession, candidate_ap_ids: set[int]) -> int:
|
||||
"""Archive any of `candidate_ap_ids` left with zero linked ImportedParts.
|
||||
|
||||
Idempotent: already-archived parts and parts that still have a link are
|
||||
left untouched. Does not commit — the caller commits as part of its own
|
||||
transaction.
|
||||
"""
|
||||
archived = 0
|
||||
for ap_id in candidate_ap_ids:
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(augmented_part_imported_parts)
|
||||
.where(augmented_part_imported_parts.c.augmented_part_id == ap_id)
|
||||
)
|
||||
if result.scalar() == 0:
|
||||
ap = await db.get(AugmentedPart, ap_id)
|
||||
if ap is not None and not ap.archived:
|
||||
ap.archived = True
|
||||
archived += 1
|
||||
return archived
|
||||
|
||||
|
||||
async def _detach_and_archive_orphans(
|
||||
db: AsyncSession, imported_part_ids: set[str]
|
||||
) -> int:
|
||||
"""Detach `imported_part_ids` from any AugmentedPart association and archive
|
||||
AugmentedParts left with zero remaining ImportedPart links as a result.
|
||||
|
||||
Deletes the association rows directly rather than relying on the
|
||||
ImportedPart row's ON DELETE CASCADE, since SQLite (this app's dev/test
|
||||
database) doesn't enforce FK actions by default. Safe to call whether or
|
||||
not the ImportedPart row(s) still exist. Does not commit — the caller
|
||||
commits as part of its own transaction.
|
||||
"""
|
||||
if not imported_part_ids:
|
||||
return 0
|
||||
|
||||
result = await db.execute(
|
||||
select(augmented_part_imported_parts.c.augmented_part_id).where(
|
||||
augmented_part_imported_parts.c.imported_part_id.in_(imported_part_ids)
|
||||
)
|
||||
)
|
||||
candidate_ap_ids: set[int] = {row[0] for row in result.all()}
|
||||
|
||||
await db.execute(
|
||||
augmented_part_imported_parts.delete().where(
|
||||
augmented_part_imported_parts.c.imported_part_id.in_(imported_part_ids)
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
return await _archive_orphans(db, candidate_ap_ids)
|
||||
|
||||
|
||||
async def delete_imported_part_and_archive_orphans(
|
||||
db: AsyncSession, imported_part: ImportedPart
|
||||
) -> int:
|
||||
"""Delete `imported_part` and archive any AugmentedPart left with no
|
||||
remaining ImportedPart links as a result.
|
||||
|
||||
Captures affected AugmentedPart ids before deleting so this is correct
|
||||
regardless of whether the DB enforces the association table's ON DELETE
|
||||
CASCADE. Returns the number of AugmentedParts newly archived.
|
||||
"""
|
||||
inventory_id = imported_part.InventoryID
|
||||
result = await db.execute(
|
||||
select(augmented_part_imported_parts.c.augmented_part_id).where(
|
||||
augmented_part_imported_parts.c.imported_part_id == inventory_id
|
||||
)
|
||||
)
|
||||
candidate_ap_ids: set[int] = {row[0] for row in result.all()}
|
||||
|
||||
await db.delete(imported_part)
|
||||
await db.execute(
|
||||
augmented_part_imported_parts.delete().where(
|
||||
augmented_part_imported_parts.c.imported_part_id == inventory_id
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
archived = await _archive_orphans(db, candidate_ap_ids)
|
||||
await db.commit()
|
||||
return archived
|
||||
|
||||
|
||||
async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
# ------------------------------------------------------------------ phase 1
|
||||
|
||||
@@ -282,42 +368,10 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
current_ids: set[str] = {row[0] for row in result.all()}
|
||||
|
||||
orphaned_import_ids = all_linked_ids - current_ids
|
||||
deleted = 0
|
||||
archived = 0
|
||||
|
||||
if orphaned_import_ids:
|
||||
# Which AugmentedParts are affected?
|
||||
result = await db.execute(
|
||||
select(augmented_part_imported_parts.c.augmented_part_id).where(
|
||||
augmented_part_imported_parts.c.imported_part_id.in_(
|
||||
orphaned_import_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
orphaned_ap_ids: set[int] = {row[0] for row in result.all()}
|
||||
|
||||
# Remove the stale association rows
|
||||
await db.execute(
|
||||
augmented_part_imported_parts.delete().where(
|
||||
augmented_part_imported_parts.c.imported_part_id.in_(
|
||||
orphaned_import_ids
|
||||
)
|
||||
)
|
||||
)
|
||||
await db.flush()
|
||||
|
||||
# Delete AugmentedParts that now have no remaining associations
|
||||
for ap_id in orphaned_ap_ids:
|
||||
result = await db.execute(
|
||||
select(func.count())
|
||||
.select_from(augmented_part_imported_parts)
|
||||
.where(augmented_part_imported_parts.c.augmented_part_id == ap_id)
|
||||
)
|
||||
if result.scalar() == 0:
|
||||
ap = await db.get(AugmentedPart, ap_id)
|
||||
if ap is not None:
|
||||
await db.delete(ap)
|
||||
deleted += 1
|
||||
|
||||
archived = await _detach_and_archive_orphans(db, orphaned_import_ids)
|
||||
await db.commit()
|
||||
|
||||
# ------------------------------------------------------------------ phase 2
|
||||
@@ -334,7 +388,7 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
|
||||
if not unlinked_ids:
|
||||
return AugmentedPartSyncResult(
|
||||
created=created, deleted=deleted, skipped=skipped
|
||||
created=created, archived=archived, skipped=skipped
|
||||
)
|
||||
|
||||
result = await db.execute(
|
||||
@@ -385,4 +439,4 @@ async def sync(db: AsyncSession) -> AugmentedPartSyncResult:
|
||||
|
||||
await db.commit()
|
||||
|
||||
return AugmentedPartSyncResult(created=created, deleted=deleted, skipped=skipped)
|
||||
return AugmentedPartSyncResult(created=created, archived=archived, skipped=skipped)
|
||||
@@ -113,6 +113,8 @@ async def build(
|
||||
other = other_parts.get(other_id)
|
||||
if other is None or other.part_type_id == scope_type.id:
|
||||
continue # scope-to-scope fits have no column
|
||||
if other.archived:
|
||||
continue # archived parts don't show up as compatible accessories
|
||||
by_type = cell_map[scope_id].setdefault(other.part_type_id, {})
|
||||
existing = by_type.get(other.id)
|
||||
label = fit.label.value
|
||||
|
||||
@@ -7,11 +7,24 @@
|
||||
<td>{{ ap.scope_model.name if ap.scope_model else '—' }}</td>
|
||||
<td>{{ ap.part_type.name }}</td>
|
||||
<td>{{ ap.sub_part_type.name if ap.sub_part_type else '—' }}</td>
|
||||
<td>{% if ap.archived %}<span class="tag">Archived</span>{% else %}—{% endif %}</td>
|
||||
<td class="row-actions">
|
||||
<button class="btn btn-ghost"
|
||||
hx-get="/admin/augmented-parts/{{ ap.id }}/edit"
|
||||
hx-target="#augmented-part-{{ ap.id }}"
|
||||
hx-swap="outerHTML">Edit</button>
|
||||
{% if ap.archived %}
|
||||
<button class="btn btn-ghost"
|
||||
hx-post="/admin/augmented-parts/{{ ap.id }}/unarchive"
|
||||
hx-target="#augmented-part-{{ ap.id }}"
|
||||
hx-swap="outerHTML">Unarchive</button>
|
||||
{% else %}
|
||||
<button class="btn btn-ghost"
|
||||
hx-post="/admin/augmented-parts/{{ ap.id }}/archive"
|
||||
hx-target="#augmented-part-{{ ap.id }}"
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Archive {{ ap.sku or ap.description }}?">Archive</button>
|
||||
{% endif %}
|
||||
<button class="btn btn-danger"
|
||||
hx-delete="/admin/augmented-parts/{{ ap.id }}"
|
||||
hx-target="#augmented-part-{{ ap.id }}"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<tr id="augmented-part-{{ ap.id }}">
|
||||
<td colspan="9">
|
||||
<td colspan="10">
|
||||
<form id="augmented-part-edit-form-{{ ap.id }}"
|
||||
hx-post="/admin/augmented-parts/{{ ap.id }}"
|
||||
hx-target="#augmented-part-{{ ap.id }}"
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
<option value="{{ spt.id }}" {{ "selected" if filter_sub_part_type_id and filter_sub_part_type_id|int == spt.id }}>{{ spt.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<label><input type="checkbox" name="include_archived" {{ "checked" if include_archived }}> Show archived</label>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
{{ sort_th("scope_model", "Model", sort_headers) }}
|
||||
{{ sort_th("part_type", "Type", sort_headers) }}
|
||||
{{ sort_th("sub_part_type", "Sub Type", sort_headers) }}
|
||||
<th>Archived</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -19,7 +20,7 @@
|
||||
{% for ap in augmented_parts %}
|
||||
{% include "admin/_augmented_part_row.html" %}
|
||||
{% else %}
|
||||
<tr><td colspan="9" class="muted">No augmented parts recorded.</td></tr>
|
||||
<tr><td colspan="10" class="muted">No augmented parts recorded.</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
<p class="muted">
|
||||
<span class="tag">{{ ap.part_type.name }}</span>
|
||||
{% if ap.sub_part_type %}<span class="tag">{{ ap.sub_part_type.name }}</span>{% endif %}
|
||||
{% if ap.archived %}<span class="tag">Archived</span>{% endif %}
|
||||
{% if ap.description %}{{ ap.description }}{% endif %}
|
||||
</p>
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
|
||||
<div class="card">
|
||||
<h2>Sync with Imported Parts</h2>
|
||||
<p class="muted">Creates Augmented Part entries for new Imported Parts, and removes entries for deleted Imported Parts.</p>
|
||||
<p class="muted">Creates Augmented Part entries for new Imported Parts, and archives entries for deleted Imported Parts.</p>
|
||||
<div class="row-actions">
|
||||
<form hx-post="/admin/augmented-parts/sync" hx-target="#augmented-part-list" hx-swap="outerHTML">
|
||||
<button type="submit" class="btn btn-primary">Sync now</button>
|
||||
|
||||
+74
-6
@@ -1660,6 +1660,7 @@ async def _default_augmented_parts_table_ctx(db: AsyncSession) -> dict:
|
||||
),
|
||||
"sort": "description",
|
||||
"dir": "asc",
|
||||
"include_archived": False,
|
||||
}
|
||||
|
||||
|
||||
@@ -1668,17 +1669,24 @@ async def augmented_parts_page(
|
||||
request: Request,
|
||||
sort: str = "description",
|
||||
dir: str = "asc",
|
||||
include_archived: bool = False,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(web_require("augmented_parts:read")),
|
||||
):
|
||||
ctx = await _augmented_part_form_ctx(db)
|
||||
total = await augmented_part_crud.count_filtered(db)
|
||||
total = await augmented_part_crud.count_filtered(
|
||||
db, include_archived=include_archived
|
||||
)
|
||||
ctx["augmented_parts"] = await augmented_part_crud.list_filtered(
|
||||
db, sort=sort, direction=dir, limit=_PAGE_SIZE
|
||||
db,
|
||||
sort=sort,
|
||||
direction=dir,
|
||||
include_archived=include_archived,
|
||||
limit=_PAGE_SIZE,
|
||||
)
|
||||
ctx["pagination"] = _paginate(
|
||||
"/admin/augmented-parts/filter",
|
||||
{"sort": sort, "dir": dir},
|
||||
{"sort": sort, "dir": dir, "include_archived": include_archived},
|
||||
1,
|
||||
_PAGE_SIZE,
|
||||
total,
|
||||
@@ -1686,7 +1694,7 @@ async def augmented_parts_page(
|
||||
)
|
||||
ctx["sort_headers"] = _sort_headers(
|
||||
"/admin/augmented-parts/filter",
|
||||
{},
|
||||
{"include_archived": include_archived},
|
||||
_AUGMENTED_PARTS_SORT_COLUMNS,
|
||||
sort,
|
||||
dir,
|
||||
@@ -1694,6 +1702,7 @@ async def augmented_parts_page(
|
||||
)
|
||||
ctx["sort"] = sort
|
||||
ctx["dir"] = dir
|
||||
ctx["include_archived"] = include_archived
|
||||
ctx["user"] = user
|
||||
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
|
||||
|
||||
@@ -1764,7 +1773,7 @@ async def sync_augmented_parts(
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=None,
|
||||
description=(
|
||||
f"Sync: {result.created} created, {result.deleted} deleted, "
|
||||
f"Sync: {result.created} created, {result.archived} archived, "
|
||||
f"{result.skipped} skipped (no type match)"
|
||||
),
|
||||
)
|
||||
@@ -1772,7 +1781,7 @@ async def sync_augmented_parts(
|
||||
job_id,
|
||||
message=(
|
||||
f"Sync complete: {result.created} created, "
|
||||
f"{result.deleted} deleted"
|
||||
f"{result.archived} archived"
|
||||
+ (
|
||||
f", {result.skipped} skipped (type not recognised)"
|
||||
if result.skipped
|
||||
@@ -1846,6 +1855,7 @@ async def filter_augmented_parts(
|
||||
scope_model_id: str = "",
|
||||
part_type_id: str = "",
|
||||
sub_part_type_id: str = "",
|
||||
include_archived: bool = False,
|
||||
sort: str = "description",
|
||||
dir: str = "asc",
|
||||
page: int = 1,
|
||||
@@ -1866,6 +1876,7 @@ async def filter_augmented_parts(
|
||||
scope_model_id=scope_model_id_int,
|
||||
part_type_id=part_type_id_int,
|
||||
sub_part_type_id=sub_part_type_id_int,
|
||||
include_archived=include_archived,
|
||||
)
|
||||
filter_params = {
|
||||
"description": description,
|
||||
@@ -1874,6 +1885,7 @@ async def filter_augmented_parts(
|
||||
"scope_model_id": scope_model_id,
|
||||
"part_type_id": part_type_id,
|
||||
"sub_part_type_id": sub_part_type_id,
|
||||
"include_archived": include_archived,
|
||||
}
|
||||
pagination = _paginate(
|
||||
"/admin/augmented-parts/filter",
|
||||
@@ -1891,6 +1903,7 @@ async def filter_augmented_parts(
|
||||
scope_model_id=scope_model_id_int,
|
||||
part_type_id=part_type_id_int,
|
||||
sub_part_type_id=sub_part_type_id_int,
|
||||
include_archived=include_archived,
|
||||
sort=sort,
|
||||
direction=dir,
|
||||
skip=pagination["skip"],
|
||||
@@ -1913,6 +1926,7 @@ async def filter_augmented_parts(
|
||||
"sort_headers": sort_headers,
|
||||
"sort": sort,
|
||||
"dir": dir,
|
||||
"include_archived": include_archived,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -2210,6 +2224,60 @@ async def delete_augmented_part(
|
||||
return Response(status_code=status.HTTP_200_OK)
|
||||
|
||||
|
||||
async def _set_augmented_part_archived(
|
||||
request: Request,
|
||||
augmented_part_id: int,
|
||||
archived: bool,
|
||||
db: AsyncSession,
|
||||
actor: User,
|
||||
):
|
||||
ap = await augmented_part_crud.get(db, augmented_part_id)
|
||||
if ap is None:
|
||||
raise HTTPException(status_code=404, detail="Augmented part not found")
|
||||
ap = await augmented_part_crud.set_archived(db, ap, archived)
|
||||
await audit_crud.log(
|
||||
db,
|
||||
actor=actor,
|
||||
action="update",
|
||||
resource_type="AugmentedPart",
|
||||
resource_id=augmented_part_id,
|
||||
description=audit_crud.describe_update(
|
||||
"AugmentedPart", augmented_part_id, {"archived": archived}
|
||||
),
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
request, "admin/_augmented_part_row.html", {"ap": ap}
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/augmented-parts/{augmented_part_id}/archive", response_class=HTMLResponse
|
||||
)
|
||||
async def archive_augmented_part(
|
||||
request: Request,
|
||||
augmented_part_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
return await _set_augmented_part_archived(
|
||||
request, augmented_part_id, True, db, actor
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/augmented-parts/{augmented_part_id}/unarchive", response_class=HTMLResponse
|
||||
)
|
||||
async def unarchive_augmented_part(
|
||||
request: Request,
|
||||
augmented_part_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
actor: User = Depends(web_require("augmented_parts:write")),
|
||||
):
|
||||
return await _set_augmented_part_archived(
|
||||
request, augmented_part_id, False, db, actor
|
||||
)
|
||||
|
||||
|
||||
def _can_write_attachment_points(user: User) -> bool:
|
||||
return user.is_superuser or "attachment_points:write" in user.permissions
|
||||
|
||||
|
||||
Reference in new issue
Block a user