The Rencontres de Bamako have run since 1994. The site carries the current biennale and every one before it, and the second half is what makes the data model interesting.
Most applications hold a current state. A festival’s site holds a sequence of past states, each of which has to stay true to its year. The 2009 edition had a curator who has since changed jobs, artists whose biographies have since been rewritten, and a rule about how many photographs a project could contain that no longer applies to anything.
None of that may drift. And it drifts by default, because the natural model — one row per person, one row per artist, settings in a settings table — makes today’s values render under yesterday’s pages.
The answer is one pivot and a discipline about what hangs off it.
What belongs to the year
Edition carries far more than dates. It carries the curatorial rules:
The test for whether something belongs on the edition rather than in site settings is precisely the one in that comment: would putting it in settings impose this rule on past editions too? A general setting is a statement about the site. A curatorial rule is a statement about one biennale, and the 2009 pages must keep describing 2009’s rule.
The featured homepage video gets the same treatment, for the same reason stated more plainly:
And the brand colour, which changes every biennale:
The consistent shape across all three: empty or zero means “as before”. That is what makes adding a field to an eleven-edition archive safe — none of the existing rows change behaviour when the column appears.
That last sentence is the argument for the whole convention. Nobody types 0 meaning “forbid everything”; they type it meaning “no limit”. Matching the semantics people actually intend costs nothing and prevents a class of data-entry accident.
Ten fields, one reader
This is the concrete incident behind the rule. The cap on images existed twice — hard-coded as “3 to 5” in the code and configured as 4 in the settings. The upload screen told the artist one number; the validator enforced the other.
The failure that produces is uniquely infuriating for the person on the receiving end: the interface says you may add a fifth photograph, and the form refuses it with no explanation that matches what you were told. It is not a crash, so nothing is logged, and it reproduces only for artists with exactly four works.
One function reads all ten fields, and every screen calls it. That is the whole fix, and it generalises: a rule stated in two places is a rule that will eventually be two rules.
The per-nature fields exist for editions that want a combination rather than a total:
And the boundary is drawn where it belongs:
The rule applies to what they present, not to what they keep. A curatorial constraint on a submission is not a storage quota, and conflating the two would make artists delete their own working material to satisfy a jury rule.
The same person, eleven times
The archivists filling in past editions found the modelling problem first:
A Participation joins a person to an edition and carries what belongs to that year. The two optional override fields are the design, and the sentence in the middle is why they are optional: nobody is obliged to rewrite the archives.
That is the pragmatic answer to a problem that has a purist answer nobody can afford. The purist answer is that every historical page should carry the biography as written at the time — which means someone has to find and retype eleven editions of biographies for a hundred people. The pragmatic answer says: the correct value wins if you have it, today’s value is served if you do not, and the field is there for the day an archivist finds the 2009 programme in a drawer.
The same pattern runs through the artist pages:
A copied column, and why it is not a mistake
There can only be one general delegate per edition — but “is this post unique?” is a property of the Poste row, and a partial unique index cannot reference a joined table.
So the flag is copied onto the participation, and the comment states the three things that make a denormalisation acceptable: why it exists (a database limitation, not convenience), what it buys (a constraint that cannot be bypassed by code that forgets to check), and who maintains it (save(), never a human — editable=False).
Compare the alternative, which is a clean() method enforcing uniqueness. That works for every write that goes through a form, and not for a management command, a data migration, a shell session, or a bulk import — which is exactly the set of tools the archivists were using.
The post proposes, the year disposes
The diagnosis here is worth extracting, because it names a specific smell. Seniority lived on the post, which is defensible reasoning — the function is what confers authority. It failed on exceptions, and the workaround was to invent a near-duplicate post to encode one year’s oddity. Production had already accumulated four of them.
Duplicated reference rows are the visible symptom of a missing override. When you find Conseiller sitting next to Conseiller du pôle, the fix is not to tidy the reference table; it is to find the attribute that should have been per-instance and was per-type.
The resolution keeps the default useful: the post still proposes the value — picking a leadership post ticks the box — and the year can override it. Nobody loses the convenience, and the exception becomes expressible.
Renaming across eleven years, carefully
Correcting a person’s name has to reach the archive pages, because those pages read denormalised name columns on the edition rows. And it must not reach too far.
The pre_save / post_save pair exists because Django gives you no other way to see the old value. And the docstring explains why the old value is the whole point: without it, a correction and an unrelated archive text look identical, and the safe choice would be to change nothing — leaving a misspelling on the public site forever.
Three constraints hold this together. re.escape on a name — because names contain dots, hyphens and parentheses, and an unescaped Marie-Ann is a character class waiting to happen. The scope limited to this person’s own participations, so a correction cannot touch an edition they had nothing to do with. And the replacement applied only where the old spelling actually occurs.
That is as far as an automatic rename can safely go, and stopping there is the right call.
Two switches, both required
Whether an artist may still edit a submission depends on two independent deadlines:
The edition’s deadline closes submissions for everyone. The person’s freeze closes one account regardless of edition. They are orthogonal, and collapsing them into one flag would make it impossible to express either case cleanly.
Both are read-only closures, never deletions:
Forty-nine files still editable after the communications deadline, and the printed version no longer matches what the database says. That is the concrete reason for the edition deadline, and it is an argument about print production rather than about security.
The reason for read-only rather than closed is consent: the artist must be able to reread what they authorised. A festival holds licences to reproduce work, and the artist’s own record of what they granted has to remain visible after the submission window shuts.
Both defaults are permissive, and both say so:
localdate(), not now(). A deadline “on 30 November” announced to artists in Bamako means the end of that day where they are. Comparing against UTC midnight would close submissions hours early for some of them, and the people affected would be the ones submitting at the last minute — which is most of them.
Observed, not declared — and re-checked at the write
Participation is not declared, it is observed. There is no join table saying who takes part in which edition; the answer is derived from the work that exists. One fewer thing to keep in sync, and one fewer way for the list of participants to disagree with the list of projects.
And the permission is checked twice, on purpose:
The dropdown is a convenience. The check is the rule. Anyone can post an edition id.
The escape hatch is explicit and narrow:
The team’s on-behalf-of screen exists precisely for the cases the deadlines are meant to catch. A deadline the organisers cannot override is a deadline that turns into a phone call and a database edit.
What carries over
- Ask whether a setting would apply to the past. If it would, it belongs on the period, not in site settings.
- Empty or zero means “as before”. It is what makes a new column safe on an eleven-year archive, and it matches what people intend when they type it.
- One reader for a rule. Two readings become two rules, and the symptom is a screen that announces one number while validation enforces another.
- Overrides are optional and fall back to today. The archive gets more correct as someone fills them in, and nothing is blocked until they do.
- Duplicated reference rows mean a missing per-instance override. Fix the model, not the reference table.
- Freeze, do not delete. Someone whose account is closed for writing still needs to reread what they consented to.
- Compare deadlines in the local date of the people they bind.