Store and serve a maintenance date as a date
The machine maintenance panel showed every date one calendar day early on this
site, and would show it a day late east of UTC.
lastmaintenancedate and nextmaintenancedate were DATETIME columns holding
midnight, and BaseModel.to_dict serialises a datetime as isoformat + 'Z', so
2026-12-15 00:00:00 went out as "2026-12-15T00:00:00Z" and the browser
converted it into local time. There is no time in a maintenance date: the form
offers a date picker, the CSV importer takes '2026-06-01', and the value is only
ever compared against other dates. Every sibling already models it correctly -
measuringtools.lastcalibrationdate, warranties.startdate and enddate are all
db.Date - so machines were the odd one out.
Migration 7d40 converts both columns; MySQL truncates a midnight time so no
data moves, and SQLite neither enforces nor needs it.
That exposed two more things. The route was assigning the raw request string to
the column, which a DATE column rejects outright - it now goes through the
strict parser from wave 2, so a mistyped maintenance date is refused with a 400
rather than stored or nulled. And BaseModel.to_dict had no branch for a plain
date, so one reached Flask's encoder and came out as an HTTP-date
("Thu, 15 Jan 2026 00:00:00 GMT") - which a browser parses back into a moment
and can still render as the day before. Dates now serialise as "2026-01-15",
with datetime checked first because it is a subclass of date.
Verified against MySQL on a live instance: the column is DATE, the API serves
2026-01-15, a mistyped date is refused with 400, and a rename on a machine that
has maintenance dates saves - the 500 from the audit-log fix in 5d81f50c, now
confirmed end to end rather than only in tests.
Scope: product
This commit is contained in:
@@ -238,3 +238,39 @@ def test_the_status_breakdown_counts_both_bays_when_collapse_is_off(
|
||||
headers=auth_headers).get_json()['data']
|
||||
assert summary['total'] == 2
|
||||
assert sum(row['count'] for row in summary['bystatus']) == 2
|
||||
|
||||
|
||||
def test_a_maintenance_date_is_served_as_a_plain_date(client, db, auth_headers):
|
||||
"""It was a DATETIME holding midnight, serialised as '...T00:00:00Z', so a
|
||||
browser west of UTC rendered every maintenance date one day early. A date
|
||||
has no time and no timezone."""
|
||||
from plugins.machines.models import Machine
|
||||
|
||||
# The create route resolves the 'machine' AssetType, which this module's
|
||||
# fixture only seeds as part of building a pair.
|
||||
if not AssetType.query.filter_by(assettype='machine').first():
|
||||
_db.session.add(AssetType(assettype='machine'))
|
||||
_db.session.commit()
|
||||
|
||||
created = client.post('/api/machines', headers=auth_headers, json={
|
||||
'assetnumber': 'MAINT-DATE-1',
|
||||
'name': 'Maintenance date probe',
|
||||
'lastmaintenancedate': '2026-01-15',
|
||||
'nextmaintenancedate': '2026-12-15',
|
||||
})
|
||||
assert created.status_code == 201, created.get_json()
|
||||
payload = created.get_json()['data']
|
||||
machineid = payload.get('machineid') or payload['machine']['machineid']
|
||||
|
||||
fetched = client.get('/api/machines/%d' % machineid,
|
||||
headers=auth_headers).get_json()['data']
|
||||
extension = fetched['machine']
|
||||
assert extension['lastmaintenancedate'] == '2026-01-15'
|
||||
assert extension['nextmaintenancedate'] == '2026-12-15'
|
||||
for value in (extension['lastmaintenancedate'],
|
||||
extension['nextmaintenancedate']):
|
||||
assert 'T' not in value and 'Z' not in value, \
|
||||
'a date must not carry a time or a timezone marker'
|
||||
|
||||
stored = db.session.get(Machine, machineid)
|
||||
assert str(stored.lastmaintenancedate) == '2026-01-15'
|
||||
|
||||
Reference in New Issue
Block a user