Adding on_website and preferred part indicators

This commit is contained in:
Ian Roddis committed 2026-09-21 14:01:23 -03:00
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")
+2
View File
@@ -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,
+18 -1
View File
@@ -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()
+2
View File
@@ -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(
+4
View File
@@ -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>
+2
View File
@@ -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
View File
@@ -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