Tables tab: a real Modified date, default sort and range filter - #2585
Merged
Merged
Conversation
Each Table records when its content last changed, in two nullable halves stamped explicitly (never auto_now): data_modified by Table.stamp_data_modified() at Apply of the Edit Journal, Bulk Upload success and column/constraint DDL; metadata_modified by api.actions.set_table_metadata in the same save that stores the Publish gate verdict (table creation and a finished review included). Publish, unpublish, embargo and role changes stamp neither. Both halves use the application's clock, so GREATEST compares like with like. Migration dataedit.0056: two ADD COLUMN ... NULL, no default, no update (checked with sqlmigrate). The tables tab shows the later half in a Modified column, "data" beneath it when only the data half is known, "–" when neither is, each with its note on hover and as text for screen readers. It becomes the default sort (-modified, title, pk), unknowns last both ways, so the bare URL now means that and the title sort is written as ?sort=table. login/listing.py gains a generic RangeFilter: two URL parameters (<name>_from, <name>_to, ISO dates, either end optional, inclusive), one control, one chip that removes both ends, one in "Filters (n)". Each filter now names its own URL params, so Listing.params, the chips and Reset stay generic. dates_within() filters on the local day the list shows; an unknown date matches no range. The Modified range sits behind "More filters". Query budget unchanged: 7 per request at every account size, 12 with every filter. Column thresholds re-measured with the Modified column (111 px): 1,160 / 1,045 / 830 (were 1,050 / 930 / 720). At a 1440 px screen Review and Datasets now drop too; recorded beside the container queries. Closes #2557 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2557 (slice 5 of #2551).
What it does
Two stamps per Table.
data_modifiedandmetadata_modifiedare nullable, neverauto_now, and are set explicitly by the write that changes them:api.actions.apply_changescolumn_add,column_altertable_change_column,table_change_constraintset_table_metadata, in the same save aspublishableThe data half goes through one helper,
Table.stamp_data_modified(): oneUPDATEof one field, with nosave(). Publish, unpublish, embargo and role changes stamp neither half, and each case is tested. Both halves use the application's clock (timezone.now()), not the database'snow(). Postgres'snow()is the start of the surrounding transaction, and the two halves are compared byGREATEST.The column. It shows the later of the two halves, with the exact timestamp on hover. A muted "data" beneath the date marks a value known only from the data half ("Last data change. Metadata edits are recorded since v1.11.0."). "–" means nothing was recorded. Both notes are also visually hidden text for screen readers. The column is placed in the stacked row too.
Default sort
-modified, title, pk, with unknown dates last in both directions (Sort(..., nulls_last=True)). The bare URL now means this sort, so the title sort is written into the URL as?sort=table. Tests that assumed the interim title default were updated.test_default_is_displayed_title_case_insensitive_then_pkwould now pass only by accident (every row ties on an unknown Modified), so it now asks forsort=tableexplicitly.Range filter, generic.
login/listing.pygainsRangeFilter: two URL parameters (<name>_from,<name>_to; ISO dates; either end optional; both inclusive), one control with two date inputs, and one chip that removes both ends. It counts once in "More filters (n)" and "Filters (n)". To make this possible, each filter now names its own URLparams. That keepsListing.params, chip removal and Reset generic.dates_within(expression, alias)filters on the local day the list shows, so 00:30 on 3 Oct in Berlin counts as 3 Oct. An unknown date matches no range, even an open one. #2559's Created filter can reuseRangeFilterwith its ownapplyfor the "before Nov 2025" rule. A date that cannot be parsed is ignored and dropped from the URL, the same as an unparseable page number. The date inputs apply onchange delay:600ms, so typing a year digit by digit sends one request.Migration
dataedit.0056_table_modified: twoADD COLUMN ... timestamp with time zone NULL, no DEFAULT, no UPDATE (checked withsqlmigrate). The deploy step is in the deploy checklist.Measured
Query budget is unchanged: 7 per request at 4, 130 and 300 Tables, including under the Modified sort, and 12 with every filter including the range (
QueryCountTests,FilterQueryCountTests).Column thresholds were re-measured with
benchmarks/tables_tab/widths.mjson the real row.seed.pynow seeds a Modified mix: 15 % unknown, 45 % data only, 40 % both. Modified is 111 px wide; its header ("Modified ↓") is wider than any date.At no screen width does the table scroll sideways. The 90–126 px page overflow at 1024 px is the known navbar user menu, the same as in Tables tab: columns collapse by the list's own width, stacked rows on small screens #2555.
Browser check (headless Chrome 152, seeded p90/max accounts)
?sort=modified(oldest first, unknowns still last), and Back restores the previous state.reverseApiBaseUrlredeclaration on history restore predates this PR.change).For you to decide
MODIFIED_RECORDED_SINCE = "v1.11.0"inlogin/tables_tab.pyis a guess at the release this ships in (v1.10.1 is already tagged). The deploy checklist says to check it before tagging.stamp_data_modified). The metadata half is set directly inset_table_metadata's existing save, as the session brief asked: no second save. Each half is written in exactly one place.apply_queued_columnraises before it reaches the change, becauseget_column_changesreadsapi_columns.exception, which does not exist (The table change queue is non-functional end to end, and applying a queued change has no permission check #2490). Its two writers are stamped and tested by calling them directly. Found on the way, not fixed:table_change_column's alter branch also crashes for every existing column, because it does"NO" in is_nullableanddescribe_columnsreturns a bool. Only its ADD branch works.Tests
api/tests/test_modification_stamps.py(18): each choke point stamps its half; a refused metadata write, a failed bulk upload, a failed queued change and an alter naming nothing stamp nothing; publish, publish with embargo, unpublish and a role grant stamp nothing. It uses its own table name, so it cannot collide with a parallel session'stest_table.login/tests/test_tables_modified.py(32): column notes, default sort and tiebreaks, both directions, every range case including the local-day boundary, chips, URL, counts, controls, and row publish/unpublish stamping nothing.LightImportTest, which fails only under the isolated-settings trick and passes alone. Vitest: 161 green.🤖 Generated with Claude Code