Settings → Language and time: saving erases a stored language the instance no longer offers #426

Closed
opened 2026-10-01 10:46:37 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-09-29 in a read-only review of postulo at 95348908d, by saving the Language and time page for somebody whose language had been withdrawn.

Checked again on 2026-10-01 against main at 0a5cad587: still present. The code is now at accounts/forms.py:178 (the site.offers() test in language_choices()), accounts/forms.py:876-879 (the ChoiceField in LocaleForm.__init__) and accounts/forms.py:860 (language_now()); templates/settings/locale.html:59 has not moved.

#120 settled that withdrawing a language must leave people's stored choice alone, so that offering it again brings them back. The model is left alone, but the settings form is not: it offers a radio only for the languages on offer, so a person whose stored language was withdrawn posts no language at all, the optional field cleans to "", and the save writes "" over the stored code. Changing only the time zone is enough.

  • Where: accounts/forms.py:177 (language_choices() skips a language site.offers() refuses, and one whose catalogue nobody has started); accounts/forms.py:879-882 (LocaleForm.__init__ replaces the field with a ChoiceField(..., required=False) built from those choices); templates/settings/locale.html:59 (one radio per offered option, so none is checked for the stored code); accounts/forms.py:863 language_now().
  • What happens: an administrator offers only en and pt-pt. A person whose profile holds de opens Settings → Language and time, changes the time zone and presses Save. The page had no value="de" radio, the POST returned 302, and profile.language became "". Offering German again later does not bring them back. The closed disclosure also shows no current language, because language_now() finds no selected row and returns an empty one.
  • Why it matters: it silently undoes the one promise #120 made about withdrawn languages, and it does so for anyone who touches the page for an unrelated reason. tests/test_offered_languages.py checks that narrowing leaves the stored value alone, but only at the model level, never through the form.
  • Fix: in LocaleForm, keep the stored language when nothing is posted for it: a clean_language() that returns self.instance.language when "language" is absent from self.data and the stored code is not among the choices. Show it in the closed row as the instance default, as #120 asked ("Show them the instance default while the language is not offered"), rather than as a blank.

Done when

  • A test in tests/test_offered_languages.py stores de, offers only en and pt-pt, posts settings:locale with a time zone and no language, and asserts profile.language == "de" and the new time zone saved; it fails today with "".
  • The same file asserts that the closed row names the instance default for that person rather than rendering empty.
Found on 2026-09-29 in a read-only review of postulo at 95348908d, by saving the Language and time page for somebody whose language had been withdrawn. Checked again on 2026-10-01 against `main` at `0a5cad587`: still present. The code is now at `accounts/forms.py:178` (the `site.offers()` test in `language_choices()`), `accounts/forms.py:876-879` (the `ChoiceField` in `LocaleForm.__init__`) and `accounts/forms.py:860` (`language_now()`); `templates/settings/locale.html:59` has not moved. #120 settled that withdrawing a language must leave people's stored choice alone, so that offering it again brings them back. The model is left alone, but the settings form is not: it offers a radio only for the languages on offer, so a person whose stored language was withdrawn posts no `language` at all, the optional field cleans to `""`, and the save writes `""` over the stored code. Changing only the time zone is enough. - **Where:** `accounts/forms.py:177` (`language_choices()` skips a language `site.offers()` refuses, and one whose catalogue nobody has started); `accounts/forms.py:879-882` (`LocaleForm.__init__` replaces the field with a `ChoiceField(..., required=False)` built from those choices); `templates/settings/locale.html:59` (one radio per offered option, so none is checked for the stored code); `accounts/forms.py:863` `language_now()`. - **What happens:** an administrator offers only `en` and `pt-pt`. A person whose profile holds `de` opens *Settings → Language and time*, changes the time zone and presses *Save*. The page had no `value="de"` radio, the POST returned 302, and `profile.language` became `""`. Offering German again later does not bring them back. The closed disclosure also shows no current language, because `language_now()` finds no selected row and returns an empty one. - **Why it matters:** it silently undoes the one promise #120 made about withdrawn languages, and it does so for anyone who touches the page for an unrelated reason. `tests/test_offered_languages.py` checks that narrowing leaves the stored value alone, but only at the model level, never through the form. - **Fix:** in `LocaleForm`, keep the stored language when nothing is posted for it: a `clean_language()` that returns `self.instance.language` when `"language"` is absent from `self.data` and the stored code is not among the choices. Show it in the closed row as the instance default, as #120 asked ("Show them the instance default while the language is not offered"), rather than as a blank. ## Done when - A test in `tests/test_offered_languages.py` stores `de`, offers only `en` and `pt-pt`, posts `settings:locale` with a time zone and no language, and asserts `profile.language == "de"` and the new time zone saved; it fails today with `""`. - The same file asserts that the closed row names the instance default for that person rather than rendering empty.
tiagoagueda added this to the 0.6.0 milestone 2026-10-01 10:46:37 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Postulo/postulo#426
No description provided.