Every account owns its dashboard arrangement from the day it exists #123
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Blocks
#125 The dashboard is a grid a person arranges by dragging
Postulo/postulo
Reference
Postulo/postulo#123
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Observation
First of three. The grid and the dragging depend on this one, because what a person drags a
widget into has to be stored somewhere that is theirs.
What exists
Profile.dashboard_widgetsis a nullable JSON list of widget keys, and it already holdsthree distinguishable answers:
Nonedefault_order[]["a", "b"]Nothing is shared between accounts today.
widgets.keys_for(profile)reads oneprofile;
widgets.Sourcesis built per request fromrequest.user; every widget computesagainst
for_user()querysets. Two accounts that have never arranged anything are notlooking at one dashboard — they are looking at the same default list of keys, each
computed against their own records. Worth saying plainly, because "unique for each user"
could be read as a leak and it is not one.
So the change this asks for is about ownership of the arrangement: giving every account
a stored arrangement of its own from the moment its profile exists, instead of an implicit
default computed from the registry.
What this asks for
A dashboard arrangement that belongs to the account from the day the account does.
Worth being careful about
Noneis load-bearing, and materialising every account destroys it. The rule it existsfor is written out in
core/widgets.py: a widget added in a later release should appearfor somebody who has never arranged their dashboard, and stay away from somebody who has
built a page deliberately. If every account has an explicit list, nobody is ever "never
arranged", and a widget added in 0.4.0 reaches nobody, for ever, unless something else
is built to carry it there.
That something has to be decided as part of this, not after it. The options, without
choosing here:
seeded from, and a later widget is offered to anybody still on an older generation;
yet" area rather than on the page, which makes the arrival explicit instead of silent;
Two things read
has_arranged()and both change meaning. Back to the standardarrangement sets
dashboard_widgets = None; the banner explaining that new widgets willappear is shown when
is_defaultis true. If nothing is everNone, reset has to mean"seed me from today's default" and the banner has to say something else or go.
Existing accounts are the population this protects. A migration that materialises
everybody freezes today's seven defaults for every account that has never touched the
setting — which is exactly the group the
Nonerule was written for. That is the trade;it should be made knowingly rather than discovered in 0.4.0 when a new widget appears for
nobody.
Profiles are created lazily, in more than one place.
Profile.objects.get_or_createappears in the settings views, and
accounts/signals.pyfills language and time zone fromSiteSettingswhen a profile is made. Whatever seeds an arrangement has to sit whereprofiles are actually created, or accounts arriving by another route — an archive import,
createsuperuser, a first SSO sign-in — get no arrangement and fall into whatever the codedoes with a missing one.
The import path carries this field.
core/importer.pycopies profile attributes out ofan archive by name. An arrangement exported before this change is a list of keys; one
exported after may not be. The importer already reads every earlier format and has to keep
doing so.
Decide the key namespace now, while it is free. Built-in keys are bare —
counters,funnel,gone_quiet. The suggestion says widgets become extensible in a later release.When they do, a third-party widget can register a key that collides with a built-in one,
and the collision lands inside stored arrangements rather than in the registry, where it
would at least raise. Choosing the shape now (
postulo:countersagainstacme:counters,or a registry that refuses an unnamespaced third-party key) costs a decision. Choosing it
after people have arranged their dashboards costs a data migration of every one of them.
A stored key that no longer exists is already handled —
keys_for()passes over itrather than breaking the page — and whatever replaces the list must keep doing that, since
uninstalling a plugin is a normal event and not an error.
Done in
a507a3b4.Ownership, which was the ask
Profile.dashboard_widgetsis non-nullable now and seeded when the profile is made — inaccounts/signals.py, where every creation path passes, and again on the way past in the arrange view for a profile that somehow has none. Both, because "from the day the account exists" should not depend on every future creation path having remembered.Your reading was right and the issue said so plainly: nothing was ever shared. What changed is that the arrangement is a stored fact rather than a computation, which is what a grid needs before a widget can be dragged into anything.
The null was load-bearing, and here is what replaced it
Of the three you listed, the seen set is the only one that survives the next thing coming:
Profile.dashboard_knownholds every key an account has already decided about. A key in neither that nor the arrangement is new to that account, whether it arrived in a release or with a plugin.An empty seen set means nothing recorded, not nothing decided. The set only ever grows and starts as every key there is, so it is never legitimately empty — while an archive from before this, or a row made by a path that missed the seeding, gives exactly that. Announcing every widget in Postulo as new to somebody who has been reading their own dashboard for a year is the worse of the two mistakes.
The trade, made knowingly
A widget added in 0.4.0 no longer appears by itself for somebody who never arranged their dashboard. It waits on the arrange page under New since you last arranged this, with two buttons — Add and Not this — and the dashboard names what is waiting.
Strictly this is one fewer thing happening without being asked: the old behaviour changed somebody's page during an upgrade. And Not this is an answer that is remembered, so nothing is offered twice.
The two things that read
has_arrangedBoth changed, as you said they would.
has_arrangedis gone;is_standard(profile)asks the value instead of its absence, which is the same question with one fewer state.Existing accounts
The migration fills in everybody who had a null, and records that they have been offered everything that exists. That freezes today's seven defaults for accounts that never touched the setting — the group the null protected — which is exactly the cost you named, paid here rather than discovered when a new widget reaches nobody. Those accounts get the seven they were already seeing, plus a seen set that will announce the eighth.
An empty list is left alone: it means deliberately cleared, which is a decision and not an absence. The reverse restores a null only for an arrangement identical to the standard one.
The keys are written out in the migration rather than read from
core.widgets— a migration that imported the registry would do something different the day a widget is renamed. It records what was true when it was written, which is all a migration can honestly record. There is a test for that.The key namespace, decided now
A bare key is Postulo's; anybody else's is
provider:key.Widget.providersays who, andregister()refuses the wrong shape at start-up: a plugin's widget with a bare key, or a namespaced key with no provider to own it.countersandacme:countersare two widgets and can coexist.You were right about the cost of deferring: a key lands inside every stored arrangement, so a collision found afterwards is a data migration of every one of them rather than an error somebody sees once.
The import path
The arrangement had never travelled in the archive at all —
PROFILE_FIELDSdid not carry it. It does now, with the seen set beside it, at format 12. A stored key that no longer exists is still passed over rather than breaking the page, which uninstalling a plugin makes an ordinary event.Also
tests/test_dashboard_ownership.py, 16 tests. Suite 4369 passed, 29 skipped; browser suite 54 passed.Next in the chain is #124: the control for placing a widget in two dimensions, which has to exist before the grid.