Adding on_website and preferred part indicators
This commit is contained in:
1 parent
13b64a2d17
commit
4fd2bb1b58
13 files changed
+331
-6
No files matched your search
@@ -0,0 +1,41 @@
|
|||||||
|
"""augmented_parts_on_website_preferred
|
||||||
|
|
||||||
|
Revision ID: 4d5e6f7a8b9c
|
||||||
|
Revises: 3c4d5e6f7a8b
|
||||||
|
Create Date: 2026-09-21 00:00:00.000000
|
||||||
|
"""
|
||||||
|
|
||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = "4d5e6f7a8b9c"
|
||||||
|
down_revision: str | None = "3c4d5e6f7a8b"
|
||||||
|
branch_labels: str | Sequence[str] | None = None
|
||||||
|
depends_on: str | Sequence[str] | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
with op.batch_alter_table("augmented_parts", schema=None) as batch_op:
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"on_website", sa.Boolean(), nullable=False, server_default=sa.false()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"preferred_part",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.false(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
with op.batch_alter_table("augmented_parts", schema=None) as batch_op:
|
||||||
|
batch_op.drop_column("preferred_part")
|
||||||
|
batch_op.drop_column("on_website")
|
||||||
@@ -59,6 +59,8 @@ async def create_augmented_part(
|
|||||||
scope_model_id=payload.scope_model_id,
|
scope_model_id=payload.scope_model_id,
|
||||||
part_type_id=payload.part_type_id,
|
part_type_id=payload.part_type_id,
|
||||||
sub_part_type_id=payload.sub_part_type_id,
|
sub_part_type_id=payload.sub_part_type_id,
|
||||||
|
on_website=payload.on_website,
|
||||||
|
preferred_part=payload.preferred_part,
|
||||||
)
|
)
|
||||||
await audit_crud.log(
|
await audit_crud.log(
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ from app.models.part_type import PartType
|
|||||||
from app.models.scope_model import ScopeModel
|
from app.models.scope_model import ScopeModel
|
||||||
from app.models.sub_part_type import SubPartType
|
from app.models.sub_part_type import SubPartType
|
||||||
|
|
||||||
_RELATIONSHIPS = ["colour", "manufacturer", "scope_model", "part_type", "sub_part_type"]
|
_RELATIONSHIPS = [
|
||||||
|
"colour",
|
||||||
|
"manufacturer",
|
||||||
|
"scope_model",
|
||||||
|
"part_type",
|
||||||
|
"sub_part_type",
|
||||||
|
"notes",
|
||||||
|
]
|
||||||
|
|
||||||
# Sortable columns for the admin list, keyed by the query-string "sort" value.
|
# Sortable columns for the admin list, keyed by the query-string "sort" value.
|
||||||
# Columns backed by a related table's name require an outer join (the FK may be
|
# Columns backed by a related table's name require an outer join (the FK may be
|
||||||
@@ -87,6 +94,7 @@ async def list_filtered(
|
|||||||
part_type_id: int | None = None,
|
part_type_id: int | None = None,
|
||||||
sub_part_type_id: int | None = None,
|
sub_part_type_id: int | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
|
on_website: bool = False,
|
||||||
sort: str = "description",
|
sort: str = "description",
|
||||||
direction: str = "asc",
|
direction: str = "asc",
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
@@ -110,6 +118,8 @@ async def list_filtered(
|
|||||||
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
||||||
if not include_archived:
|
if not include_archived:
|
||||||
q = q.where(AugmentedPart.archived.is_(False))
|
q = q.where(AugmentedPart.archived.is_(False))
|
||||||
|
if on_website:
|
||||||
|
q = q.where(AugmentedPart.on_website.is_(True))
|
||||||
q = _apply_sort(q, sort, direction)
|
q = _apply_sort(q, sort, direction)
|
||||||
result = await db.execute(q.offset(skip).limit(limit))
|
result = await db.execute(q.offset(skip).limit(limit))
|
||||||
return list(result.scalars().all())
|
return list(result.scalars().all())
|
||||||
@@ -124,6 +134,7 @@ async def count_filtered(
|
|||||||
part_type_id: int | None = None,
|
part_type_id: int | None = None,
|
||||||
sub_part_type_id: int | None = None,
|
sub_part_type_id: int | None = None,
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
|
on_website: bool = False,
|
||||||
) -> int:
|
) -> int:
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func
|
||||||
|
|
||||||
@@ -145,6 +156,8 @@ async def count_filtered(
|
|||||||
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
q = q.where(AugmentedPart.sub_part_type_id == sub_part_type_id)
|
||||||
if not include_archived:
|
if not include_archived:
|
||||||
q = q.where(AugmentedPart.archived.is_(False))
|
q = q.where(AugmentedPart.archived.is_(False))
|
||||||
|
if on_website:
|
||||||
|
q = q.where(AugmentedPart.on_website.is_(True))
|
||||||
result = await db.execute(q)
|
result = await db.execute(q)
|
||||||
return result.scalar_one()
|
return result.scalar_one()
|
||||||
|
|
||||||
@@ -220,6 +233,8 @@ async def create(
|
|||||||
scope_model_id: int | None,
|
scope_model_id: int | None,
|
||||||
part_type_id: int,
|
part_type_id: int,
|
||||||
sub_part_type_id: int | None,
|
sub_part_type_id: int | None,
|
||||||
|
on_website: bool = False,
|
||||||
|
preferred_part: bool = False,
|
||||||
) -> AugmentedPart:
|
) -> AugmentedPart:
|
||||||
ap = AugmentedPart(
|
ap = AugmentedPart(
|
||||||
sku=sku,
|
sku=sku,
|
||||||
@@ -229,6 +244,8 @@ async def create(
|
|||||||
scope_model_id=scope_model_id,
|
scope_model_id=scope_model_id,
|
||||||
part_type_id=part_type_id,
|
part_type_id=part_type_id,
|
||||||
sub_part_type_id=sub_part_type_id,
|
sub_part_type_id=sub_part_type_id,
|
||||||
|
on_website=on_website,
|
||||||
|
preferred_part=preferred_part,
|
||||||
)
|
)
|
||||||
db.add(ap)
|
db.add(ap)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class AugmentedPart(Base):
|
|||||||
# removed, so fit/attachment-point history is preserved. Hidden from list
|
# removed, so fit/attachment-point history is preserved. Hidden from list
|
||||||
# views by default.
|
# views by default.
|
||||||
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
archived: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
on_website: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
preferred_part: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
||||||
|
|
||||||
# linked imported parts — not eagerly loaded; used by sync service only
|
# linked imported parts — not eagerly loaded; used by sync service only
|
||||||
imported_parts: Mapped[list[ImportedPart]] = relationship(
|
imported_parts: Mapped[list[ImportedPart]] = relationship(
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ class AugmentedPartBase(BaseModel):
|
|||||||
scope_model_id: int | None = None
|
scope_model_id: int | None = None
|
||||||
part_type_id: int
|
part_type_id: int
|
||||||
sub_part_type_id: int | None = None
|
sub_part_type_id: int | None = None
|
||||||
|
on_website: bool = False
|
||||||
|
preferred_part: bool = False
|
||||||
|
|
||||||
|
|
||||||
class AugmentedPartCreate(AugmentedPartBase):
|
class AugmentedPartCreate(AugmentedPartBase):
|
||||||
@@ -54,6 +56,8 @@ class AugmentedPartUpdate(BaseModel):
|
|||||||
part_type_id: int | None = None
|
part_type_id: int | None = None
|
||||||
sub_part_type_id: int | None = None
|
sub_part_type_id: int | None = None
|
||||||
archived: bool | None = None
|
archived: bool | None = None
|
||||||
|
on_website: bool | None = None
|
||||||
|
preferred_part: bool | None = None
|
||||||
|
|
||||||
|
|
||||||
class AugmentedPartSyncResult(BaseModel):
|
class AugmentedPartSyncResult(BaseModel):
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
<td>{{ ap.part_type.name }}</td>
|
<td>{{ ap.part_type.name }}</td>
|
||||||
<td>{{ ap.sub_part_type.name if ap.sub_part_type else '—' }}</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>{% if ap.archived %}<span class="tag">Archived</span>{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if ap.on_website %}<span class="tag">Yes</span>{% else %}—{% endif %}</td>
|
||||||
|
<td>{% if ap.preferred_part %}<span class="tag">Yes</span>{% else %}—{% endif %}</td>
|
||||||
<td class="row-actions">
|
<td class="row-actions">
|
||||||
<button class="btn btn-ghost"
|
<button class="btn btn-ghost"
|
||||||
hx-get="/admin/augmented-parts/{{ ap.id }}/edit"
|
hx-get="/admin/augmented-parts/{{ ap.id }}/edit"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<tr id="augmented-part-{{ ap.id }}">
|
<tr id="augmented-part-{{ ap.id }}">
|
||||||
<td colspan="10">
|
<td colspan="12">
|
||||||
<form id="augmented-part-edit-form-{{ ap.id }}"
|
<form id="augmented-part-edit-form-{{ ap.id }}"
|
||||||
hx-post="/admin/augmented-parts/{{ ap.id }}"
|
hx-post="/admin/augmented-parts/{{ ap.id }}"
|
||||||
hx-target="#augmented-part-{{ ap.id }}"
|
hx-target="#augmented-part-{{ ap.id }}"
|
||||||
@@ -45,6 +45,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="check"><input type="checkbox" name="on_website" value="true" {{ "checked" if ap.on_website }}> On website</label>
|
||||||
|
<label class="check"><input type="checkbox" name="preferred_part" value="true" {{ "checked" if ap.preferred_part }}> Preferred part</label>
|
||||||
<div class="row-actions">
|
<div class="row-actions">
|
||||||
<button type="submit" class="btn btn-primary">Save</button>
|
<button type="submit" class="btn btn-primary">Save</button>
|
||||||
<button type="button" class="btn btn-ghost"
|
<button type="button" class="btn btn-ghost"
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
<label><input type="checkbox" name="include_archived" {{ "checked" if include_archived }}> Show archived</label>
|
<label><input type="checkbox" name="include_archived" {{ "checked" if include_archived }}> Show archived</label>
|
||||||
|
<label><input type="checkbox" name="on_website" {{ "checked" if on_website }}> On website</label>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@
|
|||||||
{{ sort_th("part_type", "Type", sort_headers) }}
|
{{ sort_th("part_type", "Type", sort_headers) }}
|
||||||
{{ sort_th("sub_part_type", "Sub Type", sort_headers) }}
|
{{ sort_th("sub_part_type", "Sub Type", sort_headers) }}
|
||||||
<th>Archived</th>
|
<th>Archived</th>
|
||||||
|
<th>On Website</th>
|
||||||
|
<th>Preferred</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -20,7 +22,7 @@
|
|||||||
{% for ap in augmented_parts %}
|
{% for ap in augmented_parts %}
|
||||||
{% include "admin/_augmented_part_row.html" %}
|
{% include "admin/_augmented_part_row.html" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="10" class="muted">No augmented parts recorded.</td></tr>
|
<tr><td colspan="12" class="muted">No augmented parts recorded.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -11,6 +11,8 @@
|
|||||||
<span class="tag">{{ ap.part_type.name }}</span>
|
<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.sub_part_type %}<span class="tag">{{ ap.sub_part_type.name }}</span>{% endif %}
|
||||||
{% if ap.archived %}<span class="tag">Archived</span>{% endif %}
|
{% if ap.archived %}<span class="tag">Archived</span>{% endif %}
|
||||||
|
{% if ap.on_website %}<span class="tag">On website</span>{% endif %}
|
||||||
|
{% if ap.preferred_part %}<span class="tag">Preferred</span>{% endif %}
|
||||||
{% if ap.description %}{{ ap.description }}{% endif %}
|
{% if ap.description %}{{ ap.description }}{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
<label class="check"><input type="checkbox" name="on_website" value="true"> On website</label>
|
||||||
|
<label class="check"><input type="checkbox" name="preferred_part" value="true"> Preferred part</label>
|
||||||
<button type="submit" class="btn btn-primary">Add</button>
|
<button type="submit" class="btn btn-primary">Add</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+25
-3
@@ -1674,6 +1674,7 @@ async def _default_augmented_parts_table_ctx(db: AsyncSession) -> dict:
|
|||||||
"sort": "description",
|
"sort": "description",
|
||||||
"dir": "asc",
|
"dir": "asc",
|
||||||
"include_archived": False,
|
"include_archived": False,
|
||||||
|
"on_website": False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1683,23 +1684,30 @@ async def augmented_parts_page(
|
|||||||
sort: str = "description",
|
sort: str = "description",
|
||||||
dir: str = "asc",
|
dir: str = "asc",
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
|
on_website: bool = False,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
user: User = Depends(web_require("augmented_parts:read")),
|
user: User = Depends(web_require("augmented_parts:read")),
|
||||||
):
|
):
|
||||||
ctx = await _augmented_part_form_ctx(db)
|
ctx = await _augmented_part_form_ctx(db)
|
||||||
total = await augmented_part_crud.count_filtered(
|
total = await augmented_part_crud.count_filtered(
|
||||||
db, include_archived=include_archived
|
db, include_archived=include_archived, on_website=on_website
|
||||||
)
|
)
|
||||||
ctx["augmented_parts"] = await augmented_part_crud.list_filtered(
|
ctx["augmented_parts"] = await augmented_part_crud.list_filtered(
|
||||||
db,
|
db,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
direction=dir,
|
direction=dir,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
|
on_website=on_website,
|
||||||
limit=_PAGE_SIZE,
|
limit=_PAGE_SIZE,
|
||||||
)
|
)
|
||||||
ctx["pagination"] = _paginate(
|
ctx["pagination"] = _paginate(
|
||||||
"/admin/augmented-parts/filter",
|
"/admin/augmented-parts/filter",
|
||||||
{"sort": sort, "dir": dir, "include_archived": include_archived},
|
{
|
||||||
|
"sort": sort,
|
||||||
|
"dir": dir,
|
||||||
|
"include_archived": include_archived,
|
||||||
|
"on_website": on_website,
|
||||||
|
},
|
||||||
1,
|
1,
|
||||||
_PAGE_SIZE,
|
_PAGE_SIZE,
|
||||||
total,
|
total,
|
||||||
@@ -1707,7 +1715,7 @@ async def augmented_parts_page(
|
|||||||
)
|
)
|
||||||
ctx["sort_headers"] = _sort_headers(
|
ctx["sort_headers"] = _sort_headers(
|
||||||
"/admin/augmented-parts/filter",
|
"/admin/augmented-parts/filter",
|
||||||
{"include_archived": include_archived},
|
{"include_archived": include_archived, "on_website": on_website},
|
||||||
_AUGMENTED_PARTS_SORT_COLUMNS,
|
_AUGMENTED_PARTS_SORT_COLUMNS,
|
||||||
sort,
|
sort,
|
||||||
dir,
|
dir,
|
||||||
@@ -1716,6 +1724,7 @@ async def augmented_parts_page(
|
|||||||
ctx["sort"] = sort
|
ctx["sort"] = sort
|
||||||
ctx["dir"] = dir
|
ctx["dir"] = dir
|
||||||
ctx["include_archived"] = include_archived
|
ctx["include_archived"] = include_archived
|
||||||
|
ctx["on_website"] = on_website
|
||||||
ctx["user"] = user
|
ctx["user"] = user
|
||||||
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
|
return templates.TemplateResponse(request, "admin/augmented_parts.html", ctx)
|
||||||
|
|
||||||
@@ -1730,6 +1739,8 @@ async def create_augmented_part(
|
|||||||
scope_model_id: str = Form(""),
|
scope_model_id: str = Form(""),
|
||||||
part_type_id: int = Form(...),
|
part_type_id: int = Form(...),
|
||||||
sub_part_type_id: str = Form(""),
|
sub_part_type_id: str = Form(""),
|
||||||
|
on_website: bool = Form(False),
|
||||||
|
preferred_part: bool = Form(False),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
actor: User = Depends(web_require("augmented_parts:write")),
|
actor: User = Depends(web_require("augmented_parts:write")),
|
||||||
):
|
):
|
||||||
@@ -1745,6 +1756,8 @@ async def create_augmented_part(
|
|||||||
scope_model_id=int(scope_model_id) if scope_model_id else None,
|
scope_model_id=int(scope_model_id) if scope_model_id else None,
|
||||||
part_type_id=part_type_id,
|
part_type_id=part_type_id,
|
||||||
sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None,
|
sub_part_type_id=int(sub_part_type_id) if sub_part_type_id else None,
|
||||||
|
on_website=on_website,
|
||||||
|
preferred_part=preferred_part,
|
||||||
)
|
)
|
||||||
await audit_crud.log(
|
await audit_crud.log(
|
||||||
db,
|
db,
|
||||||
@@ -1874,6 +1887,7 @@ async def filter_augmented_parts(
|
|||||||
part_type_id: str = "",
|
part_type_id: str = "",
|
||||||
sub_part_type_id: str = "",
|
sub_part_type_id: str = "",
|
||||||
include_archived: bool = False,
|
include_archived: bool = False,
|
||||||
|
on_website: bool = False,
|
||||||
sort: str = "description",
|
sort: str = "description",
|
||||||
dir: str = "asc",
|
dir: str = "asc",
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
@@ -1895,6 +1909,7 @@ async def filter_augmented_parts(
|
|||||||
part_type_id=part_type_id_int,
|
part_type_id=part_type_id_int,
|
||||||
sub_part_type_id=sub_part_type_id_int,
|
sub_part_type_id=sub_part_type_id_int,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
|
on_website=on_website,
|
||||||
)
|
)
|
||||||
filter_params = {
|
filter_params = {
|
||||||
"description": description,
|
"description": description,
|
||||||
@@ -1904,6 +1919,7 @@ async def filter_augmented_parts(
|
|||||||
"part_type_id": part_type_id,
|
"part_type_id": part_type_id,
|
||||||
"sub_part_type_id": sub_part_type_id,
|
"sub_part_type_id": sub_part_type_id,
|
||||||
"include_archived": include_archived,
|
"include_archived": include_archived,
|
||||||
|
"on_website": on_website,
|
||||||
}
|
}
|
||||||
pagination = _paginate(
|
pagination = _paginate(
|
||||||
"/admin/augmented-parts/filter",
|
"/admin/augmented-parts/filter",
|
||||||
@@ -1922,6 +1938,7 @@ async def filter_augmented_parts(
|
|||||||
part_type_id=part_type_id_int,
|
part_type_id=part_type_id_int,
|
||||||
sub_part_type_id=sub_part_type_id_int,
|
sub_part_type_id=sub_part_type_id_int,
|
||||||
include_archived=include_archived,
|
include_archived=include_archived,
|
||||||
|
on_website=on_website,
|
||||||
sort=sort,
|
sort=sort,
|
||||||
direction=dir,
|
direction=dir,
|
||||||
skip=pagination["skip"],
|
skip=pagination["skip"],
|
||||||
@@ -1945,6 +1962,7 @@ async def filter_augmented_parts(
|
|||||||
"sort": sort,
|
"sort": sort,
|
||||||
"dir": dir,
|
"dir": dir,
|
||||||
"include_archived": include_archived,
|
"include_archived": include_archived,
|
||||||
|
"on_website": on_website,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -2188,6 +2206,8 @@ async def update_augmented_part(
|
|||||||
scope_model_id: str = Form(""),
|
scope_model_id: str = Form(""),
|
||||||
part_type_id: int = Form(...),
|
part_type_id: int = Form(...),
|
||||||
sub_part_type_id: str = Form(""),
|
sub_part_type_id: str = Form(""),
|
||||||
|
on_website: bool = Form(False),
|
||||||
|
preferred_part: bool = Form(False),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
actor: User = Depends(web_require("augmented_parts:write")),
|
actor: User = Depends(web_require("augmented_parts:write")),
|
||||||
):
|
):
|
||||||
@@ -2207,6 +2227,8 @@ async def update_augmented_part(
|
|||||||
"scope_model_id": int(scope_model_id) if scope_model_id else None,
|
"scope_model_id": int(scope_model_id) if scope_model_id else None,
|
||||||
"part_type_id": part_type_id,
|
"part_type_id": part_type_id,
|
||||||
"sub_part_type_id": int(sub_part_type_id) if sub_part_type_id else None,
|
"sub_part_type_id": int(sub_part_type_id) if sub_part_type_id else None,
|
||||||
|
"on_website": on_website,
|
||||||
|
"preferred_part": preferred_part,
|
||||||
}
|
}
|
||||||
await augmented_part_crud.update(db, ap, changes)
|
await augmented_part_crud.update(db, ap, changes)
|
||||||
await audit_crud.log(
|
await audit_crud.log(
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
"""`on_website` and `preferred_part` booleans on AugmentedPart: default to
|
||||||
|
False, settable via create/update (API + admin web UI), and `on_website`
|
||||||
|
is filterable in the admin listing."""
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.models.augmented_part import AugmentedPart
|
||||||
|
from app.models.manufacturer import Manufacturer
|
||||||
|
from app.models.part_colour import PartColour
|
||||||
|
from app.models.part_type import PartType
|
||||||
|
|
||||||
|
|
||||||
|
async def _seed_reference_data(db):
|
||||||
|
manufacturer = Manufacturer(name="Acme", url="https://acme.test")
|
||||||
|
colour = PartColour(name="Black")
|
||||||
|
part_type = PartType(name="Scope")
|
||||||
|
db.add_all([manufacturer, colour, part_type])
|
||||||
|
await db.flush()
|
||||||
|
return manufacturer, colour, part_type
|
||||||
|
|
||||||
|
|
||||||
|
async def _token(client, email, password="secret123"):
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/auth/login", data={"username": email, "password": password}
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200, resp.text
|
||||||
|
return resp.json()["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
async def _superuser_headers(client, make_user, email):
|
||||||
|
await make_user(email, is_superuser=True)
|
||||||
|
return {"Authorization": f"Bearer {await _token(client, email)}"}
|
||||||
|
|
||||||
|
|
||||||
|
async def _web_login(client, email, password="secret123"):
|
||||||
|
resp = await client.post("/login", data={"email": email, "password": password})
|
||||||
|
assert resp.status_code == 303
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_create_defaults_both_fields_to_false(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
manufacturer, colour, part_type = await _seed_reference_data(db)
|
||||||
|
manufacturer_id, colour_id, part_type_id = (
|
||||||
|
manufacturer.id,
|
||||||
|
colour.id,
|
||||||
|
part_type.id,
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
headers = await _superuser_headers(client, make_user, "owp-api1@example.com")
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/augmented-parts",
|
||||||
|
json={
|
||||||
|
"description": "Widget",
|
||||||
|
"colour_id": colour_id,
|
||||||
|
"manufacturer_id": manufacturer_id,
|
||||||
|
"part_type_id": part_type_id,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["on_website"] is False
|
||||||
|
assert body["preferred_part"] is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_api_create_and_update_can_set_both_fields(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
manufacturer, colour, part_type = await _seed_reference_data(db)
|
||||||
|
manufacturer_id, colour_id, part_type_id = (
|
||||||
|
manufacturer.id,
|
||||||
|
colour.id,
|
||||||
|
part_type.id,
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
headers = await _superuser_headers(client, make_user, "owp-api2@example.com")
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/augmented-parts",
|
||||||
|
json={
|
||||||
|
"description": "Widget",
|
||||||
|
"colour_id": colour_id,
|
||||||
|
"manufacturer_id": manufacturer_id,
|
||||||
|
"part_type_id": part_type_id,
|
||||||
|
"on_website": True,
|
||||||
|
"preferred_part": True,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["on_website"] is True
|
||||||
|
assert body["preferred_part"] is True
|
||||||
|
ap_id = body["id"]
|
||||||
|
|
||||||
|
resp = await client.patch(
|
||||||
|
f"/api/augmented-parts/{ap_id}",
|
||||||
|
json={"on_website": False},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
body = resp.json()
|
||||||
|
assert body["on_website"] is False
|
||||||
|
assert body["preferred_part"] is True # untouched by the partial update
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_web_admin_create_form_sets_both_checkboxes(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
manufacturer, colour, part_type = await _seed_reference_data(db)
|
||||||
|
manufacturer_id, colour_id, part_type_id = (
|
||||||
|
manufacturer.id,
|
||||||
|
colour.id,
|
||||||
|
part_type.id,
|
||||||
|
)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
await make_user("owp-web1@example.com", role_ids=[seeded["roles"]["admin"].id])
|
||||||
|
await _web_login(client, "owp-web1@example.com")
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/admin/augmented-parts",
|
||||||
|
data={
|
||||||
|
"description": "Web widget",
|
||||||
|
"colour_id": colour_id,
|
||||||
|
"manufacturer_id": manufacturer_id,
|
||||||
|
"part_type_id": part_type_id,
|
||||||
|
"on_website": "true",
|
||||||
|
"preferred_part": "true",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "Web widget" in resp.text
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_web_admin_update_form_toggles_both_fields(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
manufacturer, colour, part_type = await _seed_reference_data(db)
|
||||||
|
ap = AugmentedPart(
|
||||||
|
description="Togglable widget",
|
||||||
|
colour_id=colour.id,
|
||||||
|
manufacturer_id=manufacturer.id,
|
||||||
|
part_type_id=part_type.id,
|
||||||
|
on_website=False,
|
||||||
|
preferred_part=False,
|
||||||
|
)
|
||||||
|
db.add(ap)
|
||||||
|
await db.commit()
|
||||||
|
ap_id, manufacturer_id, colour_id, part_type_id = (
|
||||||
|
ap.id,
|
||||||
|
manufacturer.id,
|
||||||
|
colour.id,
|
||||||
|
part_type.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
await make_user("owp-web2@example.com", role_ids=[seeded["roles"]["admin"].id])
|
||||||
|
await _web_login(client, "owp-web2@example.com")
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
f"/admin/augmented-parts/{ap_id}",
|
||||||
|
data={
|
||||||
|
"description": "Togglable widget",
|
||||||
|
"colour_id": colour_id,
|
||||||
|
"manufacturer_id": manufacturer_id,
|
||||||
|
"part_type_id": part_type_id,
|
||||||
|
"on_website": "true",
|
||||||
|
# preferred_part omitted, as an unchecked checkbox would be
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
refreshed = await db.get(AugmentedPart, ap_id)
|
||||||
|
assert refreshed.on_website is True
|
||||||
|
assert refreshed.preferred_part is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_web_admin_filter_by_on_website(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
manufacturer, colour, part_type = await _seed_reference_data(db)
|
||||||
|
on_site = AugmentedPart(
|
||||||
|
description="On-site widget",
|
||||||
|
colour_id=colour.id,
|
||||||
|
manufacturer_id=manufacturer.id,
|
||||||
|
part_type_id=part_type.id,
|
||||||
|
on_website=True,
|
||||||
|
)
|
||||||
|
off_site = AugmentedPart(
|
||||||
|
description="Off-site widget",
|
||||||
|
colour_id=colour.id,
|
||||||
|
manufacturer_id=manufacturer.id,
|
||||||
|
part_type_id=part_type.id,
|
||||||
|
on_website=False,
|
||||||
|
)
|
||||||
|
db.add_all([on_site, off_site])
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
await make_user("owp-web3@example.com", role_ids=[seeded["roles"]["admin"].id])
|
||||||
|
await _web_login(client, "owp-web3@example.com")
|
||||||
|
|
||||||
|
resp = await client.get("/admin/augmented-parts")
|
||||||
|
assert "On-site widget" in resp.text
|
||||||
|
assert "Off-site widget" in resp.text
|
||||||
|
|
||||||
|
resp = await client.get("/admin/augmented-parts/filter?on_website=true")
|
||||||
|
assert resp.status_code == 200
|
||||||
|
assert "On-site widget" in resp.text
|
||||||
|
assert "Off-site widget" not in resp.text
|
||||||
|
|
||||||
|
resp = await client.get("/admin/augmented-parts/filter")
|
||||||
|
assert "On-site widget" in resp.text
|
||||||
|
assert "Off-site widget" in resp.text
|
||||||
Reference in new issue
Block a user