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:
@@ -0,0 +1,73 @@
|
||||
"""Store a maintenance date as a DATE, because that is what it is.
|
||||
|
||||
lastmaintenancedate and nextmaintenancedate were DATETIME columns holding
|
||||
midnight. 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 a browser west of
|
||||
UTC rendered it as 14 December. The maintenance panel showed every date one
|
||||
calendar day early on this site, and would show it a day LATE east of UTC.
|
||||
|
||||
There is no time in a maintenance date. Nobody records that a machine was
|
||||
serviced at midnight; the form offers a date picker, the CSV importer takes
|
||||
'2026-06-01', and the value is compared against other dates. Every sibling
|
||||
already models it correctly - measuringtools.lastcalibrationdate,
|
||||
warranties.startdate and enddate are all db.Date - so this is machines being
|
||||
the odd one out rather than a new decision.
|
||||
|
||||
As a Date, to_dict emits "2026-12-15" with no timezone marker at all, and
|
||||
nothing can shift it.
|
||||
|
||||
MySQL truncates the (midnight) time component on the ALTER, so no data moves.
|
||||
SQLite stores what it is given and does not enforce column types, so the
|
||||
conversion is a no-op there and is skipped.
|
||||
|
||||
Revision ID: 7d40_machine_maintenance_dates
|
||||
Revises: 7d39_audit_retention_key_merge
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = '7d40_machine_maintenance_dates'
|
||||
down_revision = '7d39_audit_retention_key_merge'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
COLUMNS = ('lastmaintenancedate', 'nextmaintenancedate')
|
||||
|
||||
|
||||
def _machines_table(bind):
|
||||
inspector = sa.inspect(bind)
|
||||
if 'machines' not in inspector.get_table_names():
|
||||
return None
|
||||
return {column['name'] for column in inspector.get_columns('machines')}
|
||||
|
||||
|
||||
def upgrade():
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name == 'sqlite':
|
||||
return
|
||||
present = _machines_table(bind)
|
||||
if not present:
|
||||
return
|
||||
for column in COLUMNS:
|
||||
if column in present:
|
||||
op.alter_column('machines', column,
|
||||
existing_type=sa.DateTime(),
|
||||
type_=sa.Date(),
|
||||
existing_nullable=True)
|
||||
|
||||
|
||||
def downgrade():
|
||||
bind = op.get_bind()
|
||||
if bind.dialect.name == 'sqlite':
|
||||
return
|
||||
present = _machines_table(bind)
|
||||
if not present:
|
||||
return
|
||||
for column in COLUMNS:
|
||||
if column in present:
|
||||
op.alter_column('machines', column,
|
||||
existing_type=sa.Date(),
|
||||
type_=sa.DateTime(),
|
||||
existing_nullable=True)
|
||||
Reference in New Issue
Block a user