Adding Manual connection point to allow undeclared fits
This commit is contained in:
1 parent
7e246be39c
commit
b4dcb68bea
15 files changed
+227
-23
No files matched your search
@@ -27,7 +27,7 @@ def _connection_label(ap) -> str:
|
|||||||
return f"{cc.min_in}–{cc.max_in} in"
|
return f"{cc.min_in}–{cc.max_in} in"
|
||||||
if ap.static_connection:
|
if ap.static_connection:
|
||||||
return f"{ap.static_connection.od_mm} mm OD"
|
return f"{ap.static_connection.od_mm} mm OD"
|
||||||
return "—"
|
return "Manual"
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=list[AttachmentPointRead])
|
@router.get("", response_model=list[AttachmentPointRead])
|
||||||
@@ -54,10 +54,14 @@ async def create_attachment_point(
|
|||||||
payload.clamp_connection_id,
|
payload.clamp_connection_id,
|
||||||
payload.static_connection_id,
|
payload.static_connection_id,
|
||||||
]
|
]
|
||||||
if sum(v is not None for v in conn_ids) != 1:
|
if sum(v is not None for v in conn_ids) > 1:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=400,
|
status_code=400,
|
||||||
detail="Exactly one connection (iso_thread, unc_thread, clamp_connection, or static_connection) must be provided.",
|
detail=(
|
||||||
|
"At most one connection (iso_thread, unc_thread, clamp_connection, "
|
||||||
|
"or static_connection) may be provided. Leave all unset for a "
|
||||||
|
"manual attachment point."
|
||||||
|
),
|
||||||
)
|
)
|
||||||
ap = await attachment_point_crud.create(db, **payload.model_dump())
|
ap = await attachment_point_crud.create(db, **payload.model_dump())
|
||||||
await audit_crud.log(
|
await audit_crud.log(
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ class AttachmentPoint(Base):
|
|||||||
ForeignKey("attachment_point_names.id", ondelete="RESTRICT"), nullable=False
|
ForeignKey("attachment_point_names.id", ondelete="RESTRICT"), nullable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
# Exactly one of the four below must be non-null (enforced in application layer)
|
# At most one of the four below is non-null (enforced in application layer).
|
||||||
|
# All four null means a "Manual" attachment point: no connection details,
|
||||||
|
# used to let a user assert a Fit by hand without the fit-matching algorithm
|
||||||
|
# (app.services.fit_matching) ever auto-matching it.
|
||||||
iso_thread_id: Mapped[int | None] = mapped_column(
|
iso_thread_id: Mapped[int | None] = mapped_column(
|
||||||
ForeignKey("iso_threads.id", ondelete="RESTRICT"), nullable=True, default=None
|
ForeignKey("iso_threads.id", ondelete="RESTRICT"), nullable=True, default=None
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -76,7 +76,12 @@ def _static_clamp_match(ap_a: AttachmentPoint, ap_b: AttachmentPoint) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def attachment_points_match(ap_a: AttachmentPoint, ap_b: AttachmentPoint) -> bool:
|
def attachment_points_match(ap_a: AttachmentPoint, ap_b: AttachmentPoint) -> bool:
|
||||||
"""True if these two attachment points satisfy the fit-matching algorithm."""
|
"""True if these two attachment points satisfy the fit-matching algorithm.
|
||||||
|
|
||||||
|
A "Manual" attachment point (all four connections null) always returns
|
||||||
|
False here — it never participates in algorithmic matching, only in
|
||||||
|
fits a user asserts by hand.
|
||||||
|
"""
|
||||||
# Disabled for now, the attachment_point_names should not be considered for the purposes of matching
|
# Disabled for now, the attachment_point_names should not be considered for the purposes of matching
|
||||||
# if ap_a.attachment_point_name_id == ap_b.attachment_point_name_id:
|
# if ap_a.attachment_point_name_id == ap_b.attachment_point_name_id:
|
||||||
# return False
|
# return False
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ Fits are created with label POSSIBLE. The referenced part is looked up by
|
|||||||
``product_number`` and linked to its first attachment point (chosen answer:
|
``product_number`` and linked to its first attachment point (chosen answer:
|
||||||
"sole/first AP"). If the referenced part has no attachment points at all, one
|
"sole/first AP"). If the referenced part has no attachment points at all, one
|
||||||
is created for it that mirrors the scope's attachment point's connection, so
|
is created for it that mirrors the scope's attachment point's connection, so
|
||||||
the fit has something valid to attach to. Both directions are resolved in a
|
the fit has something valid to attach to. If the scope itself has no Ocular/
|
||||||
second pass so a fit between two scopes in the same spreadsheet resolves
|
Objective attachment point (no ocular_outer_diameter_mm/objective_thread, or
|
||||||
regardless of row order.
|
an unparseable one), a Manual attachment point — no connection details, never
|
||||||
|
picked up by the fit-matching algorithm — is created on the scope under the
|
||||||
|
appropriate label so the fit still has somewhere to attach. Both directions
|
||||||
|
are resolved in a second pass so a fit between two scopes in the same
|
||||||
|
spreadsheet resolves regardless of row order.
|
||||||
|
|
||||||
Bad rows (missing key, unknown fit target, unparseable thread) are skipped and
|
Bad rows (missing key, unknown fit target, unparseable thread) are skipped and
|
||||||
reported per-row; good rows are committed as they go.
|
reported per-row; good rows are committed as they go.
|
||||||
@@ -207,6 +211,36 @@ async def _mirrored_ap(db: AsyncSession, *, augmented_part_id: int, scope_ap):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _ensure_manual_scope_ap(
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
scope_ap,
|
||||||
|
augmented_part_id: int,
|
||||||
|
label: str,
|
||||||
|
messages: list[str],
|
||||||
|
):
|
||||||
|
"""Return ``scope_ap``, creating a Manual attachment point named ``label``
|
||||||
|
on the scope if it's None.
|
||||||
|
|
||||||
|
Used so an ocular_fit/objective_fit reference still has something to link
|
||||||
|
from even when the scope has no ocular_outer_diameter_mm/objective_thread
|
||||||
|
of its own.
|
||||||
|
"""
|
||||||
|
if scope_ap is not None:
|
||||||
|
return scope_ap
|
||||||
|
apn = await apn_crud.get_by_name(db, label)
|
||||||
|
if apn is None:
|
||||||
|
apn = await apn_crud.create(db, name=label)
|
||||||
|
messages.append(
|
||||||
|
f"scope has no {label.lower()} attachment point; created a manual one to record the fit"
|
||||||
|
)
|
||||||
|
return await attachment_point_crud.create(
|
||||||
|
db,
|
||||||
|
augmented_part_id=augmented_part_id,
|
||||||
|
attachment_point_name_id=apn.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def _link_fit(
|
async def _link_fit(
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
*,
|
*,
|
||||||
@@ -218,14 +252,11 @@ async def _link_fit(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Create a POSSIBLE fit from the scope's AP to the referenced part's first AP.
|
"""Create a POSSIBLE fit from the scope's AP to the referenced part's first AP.
|
||||||
|
|
||||||
If the referenced part has no attachment points at all, one is created
|
``scope_ap`` must already exist — callers ensure this via
|
||||||
mirroring the scope's attachment point's connection.
|
``_ensure_manual_scope_ap`` before calling. If the referenced part has no
|
||||||
|
attachment points at all, one is created mirroring the scope's attachment
|
||||||
|
point's connection.
|
||||||
"""
|
"""
|
||||||
if scope_ap is None:
|
|
||||||
messages.append(
|
|
||||||
f"{scope_role}_fit '{target_sku}' skipped: scope has no {scope_role} attachment point"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
target = await augmented_part_crud.get_by_sku(db, target_sku)
|
target = await augmented_part_crud.get_by_sku(db, target_sku)
|
||||||
if target is None:
|
if target is None:
|
||||||
messages.append(f"{scope_role}_fit references unknown product '{target_sku}'")
|
messages.append(f"{scope_role}_fit references unknown product '{target_sku}'")
|
||||||
@@ -406,6 +437,13 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult
|
|||||||
try:
|
try:
|
||||||
ocular_fit = _text(row.get("ocular_fit"))
|
ocular_fit = _text(row.get("ocular_fit"))
|
||||||
if ocular_fit:
|
if ocular_fit:
|
||||||
|
ocular_ap = await _ensure_manual_scope_ap(
|
||||||
|
db,
|
||||||
|
scope_ap=ocular_ap,
|
||||||
|
augmented_part_id=part_id,
|
||||||
|
label=OCULAR_LABEL,
|
||||||
|
messages=result.messages,
|
||||||
|
)
|
||||||
fits = ocular_fit
|
fits = ocular_fit
|
||||||
if "/" in fits:
|
if "/" in fits:
|
||||||
fits = [x.strip() for x in fits.split("/")]
|
fits = [x.strip() for x in fits.split("/")]
|
||||||
@@ -422,6 +460,13 @@ async def import_scopes(db: AsyncSession, rows: list[dict]) -> ScopeImportResult
|
|||||||
)
|
)
|
||||||
objective_fit = _text(row.get("objective_fit"))
|
objective_fit = _text(row.get("objective_fit"))
|
||||||
if objective_fit:
|
if objective_fit:
|
||||||
|
objective_ap = await _ensure_manual_scope_ap(
|
||||||
|
db,
|
||||||
|
scope_ap=objective_ap,
|
||||||
|
augmented_part_id=part_id,
|
||||||
|
label=OBJECTIVE_LABEL,
|
||||||
|
messages=result.messages,
|
||||||
|
)
|
||||||
fits = objective_fit
|
fits = objective_fit
|
||||||
if "/" in fits:
|
if "/" in fits:
|
||||||
fits = [x.strip() for x in fits.split("/")]
|
fits = [x.strip() for x in fits.split("/")]
|
||||||
|
|||||||
@@ -48,4 +48,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
|
{% elif connection_type == "manual" %}
|
||||||
|
<p>No connection details — fits for this attachment point must be asserted manually.</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
{% set conn_type = "Static Connection" %}
|
{% set conn_type = "Static Connection" %}
|
||||||
{% set conn_label = ap.static_connection.od_mm ~ " mm OD" %}
|
{% set conn_label = ap.static_connection.od_mm ~ " mm OD" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set conn_type = "—" %}
|
{% set conn_type = "Manual" %}
|
||||||
{% set conn_label = "—" %}
|
{% set conn_label = "—" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<tr id="attachment-point-{{ ap.id }}">
|
<tr id="attachment-point-{{ ap.id }}">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% elif ap.unc_thread_id %}{% set current_type = "unc_thread" %}
|
{% elif ap.unc_thread_id %}{% set current_type = "unc_thread" %}
|
||||||
{% elif ap.clamp_connection_id %}{% set current_type = "clamp_connection" %}
|
{% elif ap.clamp_connection_id %}{% set current_type = "clamp_connection" %}
|
||||||
{% elif ap.static_connection_id %}{% set current_type = "static_connection" %}
|
{% elif ap.static_connection_id %}{% set current_type = "static_connection" %}
|
||||||
{% else %}{% set current_type = "" %}{% endif %}
|
{% else %}{% set current_type = "manual" %}{% endif %}
|
||||||
<tr id="attachment-point-{{ ap.id }}">
|
<tr id="attachment-point-{{ ap.id }}">
|
||||||
<td colspan="6">
|
<td colspan="6">
|
||||||
<form hx-post="/admin/attachment-points/{{ ap.id }}"
|
<form hx-post="/admin/attachment-points/{{ ap.id }}"
|
||||||
@@ -41,6 +41,7 @@
|
|||||||
<option value="unc_thread" {% if current_type == "unc_thread" %}selected{% endif %}>UNC Thread</option>
|
<option value="unc_thread" {% if current_type == "unc_thread" %}selected{% endif %}>UNC Thread</option>
|
||||||
<option value="clamp_connection" {% if current_type == "clamp_connection" %}selected{% endif %}>Clamp Connection</option>
|
<option value="clamp_connection" {% if current_type == "clamp_connection" %}selected{% endif %}>Clamp Connection</option>
|
||||||
<option value="static_connection" {% if current_type == "static_connection" %}selected{% endif %}>Static Connection</option>
|
<option value="static_connection" {% if current_type == "static_connection" %}selected{% endif %}>Static Connection</option>
|
||||||
|
<option value="manual" {% if current_type == "manual" %}selected{% endif %}>Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{# Render the current connection select pre-filled #}
|
{# Render the current connection select pre-filled #}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
{% set conn_type = "Static" %}
|
{% set conn_type = "Static" %}
|
||||||
{% set conn_label = apoint.static_connection.od_mm ~ " mm OD" %}
|
{% set conn_label = apoint.static_connection.od_mm ~ " mm OD" %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% set conn_type = "—" %}
|
{% set conn_type = "Manual" %}
|
||||||
{% set conn_label = "—" %}
|
{% set conn_label = "—" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="augmented-part-ap-{{ apoint.id }}" class="ap-item">
|
<div id="augmented-part-ap-{{ apoint.id }}" class="ap-item">
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{% elif apoint.unc_thread_id %}{% set current_type = "unc_thread" %}
|
{% elif apoint.unc_thread_id %}{% set current_type = "unc_thread" %}
|
||||||
{% elif apoint.clamp_connection_id %}{% set current_type = "clamp_connection" %}
|
{% elif apoint.clamp_connection_id %}{% set current_type = "clamp_connection" %}
|
||||||
{% elif apoint.static_connection_id %}{% set current_type = "static_connection" %}
|
{% elif apoint.static_connection_id %}{% set current_type = "static_connection" %}
|
||||||
{% else %}{% set current_type = "" %}{% endif %}
|
{% else %}{% set current_type = "manual" %}{% endif %}
|
||||||
{# _attachment_point_connection_select.html expects `ap` for pre-selection #}
|
{# _attachment_point_connection_select.html expects `ap` for pre-selection #}
|
||||||
{% set ap = apoint %}
|
{% set ap = apoint %}
|
||||||
{% set connection_type = current_type %}
|
{% set connection_type = current_type %}
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
<option value="unc_thread" {{ "selected" if current_type == "unc_thread" }}>UNC Thread</option>
|
<option value="unc_thread" {{ "selected" if current_type == "unc_thread" }}>UNC Thread</option>
|
||||||
<option value="clamp_connection" {{ "selected" if current_type == "clamp_connection" }}>Clamp Connection</option>
|
<option value="clamp_connection" {{ "selected" if current_type == "clamp_connection" }}>Clamp Connection</option>
|
||||||
<option value="static_connection" {{ "selected" if current_type == "static_connection" }}>Static Connection</option>
|
<option value="static_connection" {{ "selected" if current_type == "static_connection" }}>Static Connection</option>
|
||||||
|
<option value="manual" {{ "selected" if current_type == "manual" }}>Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div id="edit-ap-{{ apoint.id }}-connection-options">
|
<div id="edit-ap-{{ apoint.id }}-connection-options">
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
<option value="unc_thread">UNC Thread</option>
|
<option value="unc_thread">UNC Thread</option>
|
||||||
<option value="clamp_connection">Clamp Connection</option>
|
<option value="clamp_connection">Clamp Connection</option>
|
||||||
<option value="static_connection">Static Connection</option>
|
<option value="static_connection">Static Connection</option>
|
||||||
|
<option value="manual">Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div id="ap-{{ augmented_part_id }}-add-connection-options"></div>
|
<div id="ap-{{ augmented_part_id }}-add-connection-options"></div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
{%- elif ap.static_connection -%}
|
{%- elif ap.static_connection -%}
|
||||||
Static {{ ap.static_connection.od_mm }} mm OD
|
Static {{ ap.static_connection.od_mm }} mm OD
|
||||||
{%- else -%}
|
{%- else -%}
|
||||||
—
|
Manual
|
||||||
{%- endif -%}
|
{%- endif -%}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
<option value="unc_thread">UNC Thread</option>
|
<option value="unc_thread">UNC Thread</option>
|
||||||
<option value="clamp_connection">Clamp Connection</option>
|
<option value="clamp_connection">Clamp Connection</option>
|
||||||
<option value="static_connection">Static Connection</option>
|
<option value="static_connection">Static Connection</option>
|
||||||
|
<option value="manual">Manual</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<div id="add-connection-options"></div>
|
<div id="add-connection-options"></div>
|
||||||
|
|||||||
+3
-3
@@ -2387,7 +2387,7 @@ async def edit_augmented_part_attachment_point(
|
|||||||
elif apoint.static_connection_id:
|
elif apoint.static_connection_id:
|
||||||
ctx["connection_type"] = "static_connection"
|
ctx["connection_type"] = "static_connection"
|
||||||
else:
|
else:
|
||||||
ctx["connection_type"] = ""
|
ctx["connection_type"] = "manual"
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_augmented_part_ap_row_edit.html", ctx
|
request, "admin/_augmented_part_ap_row_edit.html", ctx
|
||||||
)
|
)
|
||||||
@@ -3242,7 +3242,7 @@ def _ap_connection_label(ap) -> str:
|
|||||||
return f"{cc.min_in}–{cc.max_in} in"
|
return f"{cc.min_in}–{cc.max_in} in"
|
||||||
if ap.static_connection:
|
if ap.static_connection:
|
||||||
return f"{ap.static_connection.od_mm} mm OD"
|
return f"{ap.static_connection.od_mm} mm OD"
|
||||||
return "—"
|
return "Manual"
|
||||||
|
|
||||||
|
|
||||||
@router.get("/attachment-points", response_class=HTMLResponse)
|
@router.get("/attachment-points", response_class=HTMLResponse)
|
||||||
@@ -3339,7 +3339,7 @@ async def edit_attachment_point_row(
|
|||||||
elif ap.static_connection_id:
|
elif ap.static_connection_id:
|
||||||
ctx["connection_type"] = "static_connection"
|
ctx["connection_type"] = "static_connection"
|
||||||
else:
|
else:
|
||||||
ctx["connection_type"] = ""
|
ctx["connection_type"] = "manual"
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
request, "admin/_attachment_point_row_edit.html", ctx
|
request, "admin/_attachment_point_row_edit.html", ctx
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
from app.models.attachment_point_name import AttachmentPointName
|
||||||
|
from app.models.augmented_part import AugmentedPart
|
||||||
|
from app.models.iso_thread import IsoThread
|
||||||
|
from app.models.manufacturer import Manufacturer
|
||||||
|
from app.models.part_colour import PartColour
|
||||||
|
from app.models.part_type import PartType
|
||||||
|
|
||||||
|
|
||||||
|
async def _seed_part(db):
|
||||||
|
manufacturer = Manufacturer(name="Acme", url="https://acme.test")
|
||||||
|
colour = PartColour(name="Black")
|
||||||
|
part_type = PartType(name="Flip Cover")
|
||||||
|
apn = AttachmentPointName(name="Mount")
|
||||||
|
iso = IsoThread(diameter_mm=52.0, pitch_mm=0.75)
|
||||||
|
db.add_all([manufacturer, colour, part_type, apn, iso])
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
part = AugmentedPart(
|
||||||
|
description="Flip cover",
|
||||||
|
colour_id=colour.id,
|
||||||
|
manufacturer_id=manufacturer.id,
|
||||||
|
part_type_id=part_type.id,
|
||||||
|
)
|
||||||
|
db.add(part)
|
||||||
|
await db.flush()
|
||||||
|
await db.commit()
|
||||||
|
return part.id, apn.id, iso.id
|
||||||
|
|
||||||
|
|
||||||
|
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 test_create_manual_attachment_point_with_no_connection_succeeds(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
await make_user("root@example.com", is_superuser=True)
|
||||||
|
headers = {"Authorization": f"Bearer {await _token(client, 'root@example.com')}"}
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
part_id, apn_id, _ = await _seed_part(db)
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/attachment-points",
|
||||||
|
json={
|
||||||
|
"augmented_part_id": part_id,
|
||||||
|
"attachment_point_name_id": apn_id,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
body = resp.json()
|
||||||
|
assert body["iso_thread_id"] is None
|
||||||
|
assert body["unc_thread_id"] is None
|
||||||
|
assert body["clamp_connection_id"] is None
|
||||||
|
assert body["static_connection_id"] is None
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_attachment_point_with_two_connections_rejected(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
await make_user("root2@example.com", is_superuser=True)
|
||||||
|
headers = {"Authorization": f"Bearer {await _token(client, 'root2@example.com')}"}
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
part_id, apn_id, iso_id = await _seed_part(db)
|
||||||
|
from app.crud import static_connection as static_connection_crud
|
||||||
|
|
||||||
|
static = await static_connection_crud.create(db, od_mm=40.0, od_in=1.57)
|
||||||
|
static_id = static.id
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/attachment-points",
|
||||||
|
json={
|
||||||
|
"augmented_part_id": part_id,
|
||||||
|
"attachment_point_name_id": apn_id,
|
||||||
|
"iso_thread_id": iso_id,
|
||||||
|
"static_connection_id": static_id,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 400
|
||||||
|
assert "at most one" in resp.json()["detail"].lower()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_create_attachment_point_with_one_connection_still_works(
|
||||||
|
client, seeded, make_user, db_sessionmaker
|
||||||
|
):
|
||||||
|
await make_user("root3@example.com", is_superuser=True)
|
||||||
|
headers = {"Authorization": f"Bearer {await _token(client, 'root3@example.com')}"}
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
part_id, apn_id, iso_id = await _seed_part(db)
|
||||||
|
|
||||||
|
resp = await client.post(
|
||||||
|
"/api/attachment-points",
|
||||||
|
json={
|
||||||
|
"augmented_part_id": part_id,
|
||||||
|
"attachment_point_name_id": apn_id,
|
||||||
|
"iso_thread_id": iso_id,
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
assert resp.status_code == 201, resp.text
|
||||||
|
assert resp.json()["iso_thread_id"] == iso_id
|
||||||
@@ -284,6 +284,41 @@ async def test_fit_reference_without_attachment_points_creates_matching_one(
|
|||||||
assert acc_aps[0].id in linked_ap_ids
|
assert acc_aps[0].id in linked_ap_ids
|
||||||
|
|
||||||
|
|
||||||
|
async def test_scope_fit_without_scope_attachment_point_creates_manual_one(
|
||||||
|
db_sessionmaker,
|
||||||
|
):
|
||||||
|
async with db_sessionmaker() as db:
|
||||||
|
_, acc_ap = await _make_accessory(db, "ACC-1", od_mm=40)
|
||||||
|
|
||||||
|
# No ocular_outer_diameter_mm, so the scope itself has no Ocular AP,
|
||||||
|
# but an ocular_fit reference is still given.
|
||||||
|
data = _workbook(
|
||||||
|
[_scope_row(ocular_outer_diameter_mm=None, ocular_fit="ACC-1")]
|
||||||
|
)
|
||||||
|
result = await scope_import_service.import_from_bytes(db, data)
|
||||||
|
assert result.created == 1
|
||||||
|
msgs = " ".join(result.rows[0].messages)
|
||||||
|
assert "created a manual one to record the fit" in msgs
|
||||||
|
|
||||||
|
part = await augmented_part_crud.get_by_sku(db, "SCOPE-A")
|
||||||
|
scope_aps = await attachment_point_crud.list_for_augmented_part(db, part.id)
|
||||||
|
by_label = {ap.attachment_point_name.name: ap for ap in scope_aps}
|
||||||
|
assert "Ocular" in by_label
|
||||||
|
ocular_ap = by_label["Ocular"]
|
||||||
|
assert ocular_ap.iso_thread is None
|
||||||
|
assert ocular_ap.unc_thread is None
|
||||||
|
assert ocular_ap.clamp_connection is None
|
||||||
|
assert ocular_ap.static_connection is None
|
||||||
|
# Objective still gets its normal thread-based AP.
|
||||||
|
assert "Objective" in by_label
|
||||||
|
|
||||||
|
fits = await fit_crud.list_for_part(db, part.id)
|
||||||
|
assert len(fits) == 1
|
||||||
|
linked_ap_ids = {fits[0].attachment_point_a_id, fits[0].attachment_point_b_id}
|
||||||
|
assert ocular_ap.id in linked_ap_ids
|
||||||
|
assert acc_ap.id in linked_ap_ids
|
||||||
|
|
||||||
|
|
||||||
async def test_unc_objective_thread_creates_unc_attachment_point(db_sessionmaker):
|
async def test_unc_objective_thread_creates_unc_attachment_point(db_sessionmaker):
|
||||||
async with db_sessionmaker() as db:
|
async with db_sessionmaker() as db:
|
||||||
data = _workbook([_scope_row(objective_thread="1.750-32 UNS")])
|
data = _workbook([_scope_row(objective_thread="1.750-32 UNS")])
|
||||||
|
|||||||
Reference in new issue
Block a user