Adding Manual connection point to allow undeclared fits

This commit is contained in:
Ian Roddis committed 2026-08-18 16:57:28 -03:00
1 parent 7e246be39c
commit b4dcb68bea
15 files changed
+227 -23

No files matched your search

+7 -3
View File
@@ -27,7 +27,7 @@ def _connection_label(ap) -> str:
return f"{cc.min_in}–{cc.max_in} in"
if ap.static_connection:
return f"{ap.static_connection.od_mm} mm OD"
return "—"
return "Manual"
@router.get("", response_model=list[AttachmentPointRead])
@@ -54,10 +54,14 @@ async def create_attachment_point(
payload.clamp_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(
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())
await audit_crud.log(
+4 -1
View File
@@ -21,7 +21,10 @@ class AttachmentPoint(Base):
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(
ForeignKey("iso_threads.id", ondelete="RESTRICT"), nullable=True, default=None
)
+6 -1
View File
@@ -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:
"""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
# if ap_a.attachment_point_name_id == ap_b.attachment_point_name_id:
# return False
+55 -10
View File
@@ -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:
"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
the fit has something valid to attach to. Both directions are resolved in a
second pass so a fit between two scopes in the same spreadsheet resolves
regardless of row order.
the fit has something valid to attach to. If the scope itself has no Ocular/
Objective attachment point (no ocular_outer_diameter_mm/objective_thread, or
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
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(
db: AsyncSession,
*,
@@ -218,14 +252,11 @@ async def _link_fit(
) -> None:
"""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
mirroring the scope's attachment point's connection.
``scope_ap`` must already exist — callers ensure this via
``_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)
if target is None:
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:
ocular_fit = _text(row.get("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
if "/" in fits:
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"))
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
if "/" in fits:
fits = [x.strip() for x in fits.split("/")]
@@ -48,4 +48,6 @@
{% endfor %}
</select>
</label>
{% elif connection_type == "manual" %}
<p>No connection details — fits for this attachment point must be asserted manually.</p>
{% endif %}
@@ -15,7 +15,7 @@
{% set conn_type = "Static Connection" %}
{% set conn_label = ap.static_connection.od_mm ~ " mm OD" %}
{% else %}
{% set conn_type = "—" %}
{% set conn_type = "Manual" %}
{% set conn_label = "—" %}
{% endif %}
<tr id="attachment-point-{{ ap.id }}">
@@ -2,7 +2,7 @@
{% elif ap.unc_thread_id %}{% set current_type = "unc_thread" %}
{% elif ap.clamp_connection_id %}{% set current_type = "clamp_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 }}">
<td colspan="6">
<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="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="manual" {% if current_type == "manual" %}selected{% endif %}>Manual</option>
</select>
</label>
{# Render the current connection select pre-filled #}
@@ -15,7 +15,7 @@
{% set conn_type = "Static" %}
{% set conn_label = apoint.static_connection.od_mm ~ " mm OD" %}
{% else %}
{% set conn_type = "—" %}
{% set conn_type = "Manual" %}
{% set conn_label = "—" %}
{% endif %}
<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.clamp_connection_id %}{% set current_type = "clamp_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 #}
{% set ap = apoint %}
{% set connection_type = current_type %}
@@ -29,6 +29,7 @@
<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="static_connection" {{ "selected" if current_type == "static_connection" }}>Static Connection</option>
<option value="manual" {{ "selected" if current_type == "manual" }}>Manual</option>
</select>
</label>
<div id="edit-ap-{{ apoint.id }}-connection-options">
@@ -31,6 +31,7 @@
<option value="unc_thread">UNC Thread</option>
<option value="clamp_connection">Clamp Connection</option>
<option value="static_connection">Static Connection</option>
<option value="manual">Manual</option>
</select>
</label>
<div id="ap-{{ augmented_part_id }}-add-connection-options"></div>
@@ -8,7 +8,7 @@
{%- elif ap.static_connection -%}
Static {{ ap.static_connection.od_mm }} mm OD
{%- else -%}
—
Manual
{%- endif -%}
{%- endmacro %}
@@ -38,6 +38,7 @@
<option value="unc_thread">UNC Thread</option>
<option value="clamp_connection">Clamp Connection</option>
<option value="static_connection">Static Connection</option>
<option value="manual">Manual</option>
</select>
</label>
<div id="add-connection-options"></div>
+3 -3
View File
@@ -2387,7 +2387,7 @@ async def edit_augmented_part_attachment_point(
elif apoint.static_connection_id:
ctx["connection_type"] = "static_connection"
else:
ctx["connection_type"] = ""
ctx["connection_type"] = "manual"
return templates.TemplateResponse(
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"
if ap.static_connection:
return f"{ap.static_connection.od_mm} mm OD"
return "—"
return "Manual"
@router.get("/attachment-points", response_class=HTMLResponse)
@@ -3339,7 +3339,7 @@ async def edit_attachment_point_row(
elif ap.static_connection_id:
ctx["connection_type"] = "static_connection"
else:
ctx["connection_type"] = ""
ctx["connection_type"] = "manual"
return templates.TemplateResponse(
request, "admin/_attachment_point_row_edit.html", ctx
)