A rendered document points at whatever made it, not at one of two columns #130
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
#133 A document is a kind, and a kind is a plugin
Postulo/postulo
Reference
Postulo/postulo#130
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 prerequisite. Before a third kind of document can exist, the documents app has to stop
naming its two kinds in foreign keys.
What exists
The app already holds two authored kinds —
CVandCoverLetter— and the second carriesfour shapes of its own in
LetterKind. So it is not quite "conceived for CVs". What isconceived for exactly two kinds is the plumbing around them, and it says so in three places:
RenderedDocument, the frozen PDF, records what it came from with a pair of nullableforeign keys:
DocumentCopy, the row saying where a copy went, does the same for the other axis:archiving.pythen asks in code what the columns could not say:Add portfolios, emails and reports and each of those grows: two more nullable columns on
two models, an
isinstancebranch wherever one is read, and a migration each time.The app already contains the argument against this.
CVItemuses a generic relation,and the docstring says why:
That is this problem, one model over, decided the other way.
What this asks for
One polymorphic link from a rendered document to whatever produced it, and one from a copy
to whatever was copied — so that a new kind of document is a new package rather than a new
column.
Worth being careful about
Two migrations of live data, not one.
RenderedDocument.cvand.cover_letterholdreal rows on every instance, and so does
DocumentCopy. Moving them to a generic link meanswriting the content type and object id across, keeping the old columns until the new ones
are proven, and a reverse that works —
core/migrations/0008is the recent precedent forthe shape.
on_deletesemantics are not the same and are load-bearing.RenderedDocument.cvisSET_NULL: deleting a CV must not delete the PDF an employer received, and that rule is thewhole point of the model.
DocumentCopy.renderedisCASCADE. A generic foreign key has noon_deleteat all, so both behaviours have to be re-created deliberately — aGenericRelationfor the cascade, and something explicit for the one that must notcascade. Getting this wrong deletes somebody's record of what they sent.
Queries get worse before they get better.
related_name="renders"andselect_related("cv")are cheap; a generic link is two columns and no join. Where theinterface lists documents with what made them, this wants a prefetch rather than a loop.
The export carries these links.
core/export.pywrites documents with theirrelationships and the importer reconnects them by local id. Both move with this, and the
importer still has to read an archive written yesterday.
A kind that produces nothing is still a kind. An
UploadedDocumentcame from a file, notfrom an authored thing; the generic link has to be legitimately empty and every reader has to
cope, exactly as
keys_for()copes with a widget key it does not recognise.One link from a render to whatever made it, one from a copy to whatever was copied. The
app's own argument, applied one model over:
RenderedDocument.cv/.cover_letter→source.DocumentCopy.rendered/.upload→document.archiving._lookup'sisinstance→ a content type. The next kind of document isa package.
The two
on_deletebehavioursThe part with the most care in it, because they are not the same one and a generic
foreign key has neither:
RenderedDocument.cvwasSET_NULL. Deleting a CV must not delete the PDF an employerreceived — that is what the model is for. So a receiver clears the link, and clears it
rather than leaving it dangling: a
GenericForeignKeyreads a missing row asNoneon itsown, but the columns would keep pointing at an id another row could later take.
DocumentCopy.renderedwasCASCADE. So the cascade lives on aGenericRelationateach document, and deleting a PDF still takes the rows saying where its copies went.
Each direction is a test, because getting either backwards loses somebody's record of what
they sent.
The other four warnings
Two migrations of live data, not one. Add, carry across, then remove — with a reverse
that carries back, following
core/0008. The autodetector wanted to drop the columns beforeanything had read them, which is a one-way door on somebody's instance.
Queries get worse before they get better. They did not get worse. A generic link has no
join to
select_related, socopies_foris one batch keyed on content type: a page listingdocuments with where their copies went pays two queries rather than one per row, and
django_assert_num_queries(1)holds it there.The export carries these links. Format 10 writes a
source_kindand asource_refinstead of two id columns, and the importer reads both shapes — an archive made last month is
still an archive. That reading is a named function now rather than eleven lines inside a long
one, which is what let the old shape be tested at all.
A kind that produces nothing is still a kind. An uploaded document came from a file, and
a render whose source was deleted has none either; both are ordinary states.
source_labelis what a reader asks instead of
isinstance, so a kind arriving later is one it alreadydescribes.
The check constraint saying "exactly one of these two" is gone with the two: a generic link
is exactly one thing by construction, which is one fewer rule to widen when the third kind
arrives.
14 tests. Shipped in
b609df1on0.3.0, withmainkept level. #132 is next in thatchain, and now has somewhere to stand.