Remediate the 2026-09-03 feature-test findings

This commit is contained in:
2026-09-03 17:14:53 -04:00
parent ddf41c3e65
commit ff9ae693a6
112 changed files with 5182 additions and 857 deletions

View File

@@ -20,6 +20,7 @@ from shopdb.api import (
success_response, error_response, paginated_response, ErrorCodes,
get_pagination_params, paginate_query,
require_permission, apply_import_timestamps,
DateValueError, parse_date_fields,
)
from ..models import MeasuringTool, MeasuringToolType, derive_status, STATUS_COLORS
@@ -27,16 +28,6 @@ from ..models import MeasuringTool, MeasuringToolType, derive_status, STATUS_COL
measuringtools_bp = Blueprint('measuringtools', __name__)
def _parse_date(value):
"""Accept 'YYYY-MM-DD' (or None/empty) -> date or None."""
if not value:
return None
try:
return datetime.strptime(str(value)[:10], '%Y-%m-%d').date()
except (ValueError, TypeError):
return None
def _merged(tool, today=None):
"""Asset core dict with the measuringtools extension nested under 'measuringtool'."""
result = tool.asset.to_dict() if tool.asset else {}
@@ -191,19 +182,32 @@ def list_tools():
query = query.filter(Asset.statusid == status_id)
query = query.order_by(Asset.assetnumber)
items, total = paginate_query(query, page, per_page)
today = date.today()
data = [_merged(tool, today) for tool in items]
# Calibration status is derived, so it cannot be a SQL filter - apply it
# to the built rows. Filtering post-pagination is acceptable here because
# the dataset is a gage lab's worth of tools, not a fleet.
# Calibration status is DERIVED (see derive_status), so it cannot be a SQL
# filter. It used to be applied to the rows of the current page, which
# meant the page showed the matches that happened to fall on it while
# meta.pagination.total counted the unfiltered set: asking for 'overdue'
# with one overdue tool in sixteen returned an empty page 1, a total of 16
# and eight pages, with the actual row on page 3. A filter that hides
# matching rows and miscounts the rest is worse than no filter.
#
# So when this filter is in play the whole matching set is built and
# filtered before paging. That is the same tradeoff the previous comment
# named - a gage lab's worth of tools, not a fleet - spent where it buys
# correct answers rather than a cheaper query.
calibrationstatus = request.args.get('calibrationstatus')
if calibrationstatus:
data = [d for d in data
if d['measuringtool']['calibrationstatus'] == calibrationstatus]
matched = [_merged(tool, today) for tool in query.all()]
matched = [row for row in matched
if row['measuringtool']['calibrationstatus'] == calibrationstatus]
total = len(matched)
start = (page - 1) * per_page
data = matched[start:start + per_page]
return paginated_response(data, page, per_page, total)
items, total = paginate_query(query, page, per_page)
data = [_merged(tool, today) for tool in items]
return paginated_response(data, page, per_page, total)
@@ -240,6 +244,12 @@ def _asset_type_id():
def create_tool():
"""Create a measuring tool: one Asset core row + one extension row."""
data = request.get_json() or {}
# Refused, not nulled - see the note at the update site.
try:
dates = parse_date_fields(
data, ('lastcalibrationdate', 'nextcalibrationdate'))
except DateValueError as bad:
return error_response(ErrorCodes.VALIDATION_ERROR, str(bad))
if not data.get('assetnumber'):
return error_response(ErrorCodes.VALIDATION_ERROR, 'assetnumber is required')
if Asset.query.filter_by(assetnumber=data['assetnumber']).first():
@@ -275,8 +285,8 @@ def create_tool():
assetid=asset.assetid,
measuringtooltypeid=data.get('measuringtooltypeid'),
calibrationintervaldays=data.get('calibrationintervaldays'),
lastcalibrationdate=_parse_date(data.get('lastcalibrationdate')),
nextcalibrationdate=_parse_date(data.get('nextcalibrationdate')),
lastcalibrationdate=dates.get('lastcalibrationdate'),
nextcalibrationdate=dates.get('nextcalibrationdate'),
calibrationprovider=(data.get('calibrationprovider') or '').strip() or None,
notes=(data.get('notes') or '').strip() or None,
)
@@ -309,6 +319,12 @@ def update_tool(tool_id: int):
return error_response(ErrorCodes.NOT_FOUND,
f'Measuring tool {tool_id} not found', http_code=404)
data = request.get_json() or {}
# Refused, not nulled - see the note at the update site.
try:
dates = parse_date_fields(
data, ('lastcalibrationdate', 'nextcalibrationdate'))
except DateValueError as bad:
return error_response(ErrorCodes.VALIDATION_ERROR, str(bad))
asset = tool.asset
# An UPDATE may not blank the asset number. Create validates it and the
@@ -339,9 +355,11 @@ def update_tool(tool_id: int):
if getattr(tool, key) != data[key]:
changes[key] = {'old': getattr(tool, key), 'new': data[key]}
setattr(tool, key, data[key])
for key in ('lastcalibrationdate', 'nextcalibrationdate'):
if key in data:
setattr(tool, key, _parse_date(data[key]))
# Refused, not nulled: a mistyped calibration date used to store NULL and
# answer 200, which flipped the tool to 'unknown' and dropped it out of the
# calibration report and the map overlay.
for key, parsed in dates.items():
setattr(tool, key, parsed)
if changes:
AuditLog.log('updated', 'MeasuringTool', entityid=tool.measuringtoolid,