Skip to content

Fix MultiValueDictKeyError when radio group is unselected in save_set…#366

Open
jamesmulcahy wants to merge 2 commits intoNSPManager:develfrom
jamesmulcahy:fix-optimistic-mode-bug
Open

Fix MultiValueDictKeyError when radio group is unselected in save_set…#366
jamesmulcahy wants to merge 2 commits intoNSPManager:develfrom
jamesmulcahy:fix-optimistic-mode-bug

Conversation

@jamesmulcahy
Copy link
Copy Markdown
Contributor

…tings

Radio button groups (optimistic_mode, show_screensaver_inside/outside_temperature, turn_on_behavior) are absent from POST data when neither option is pre-selected (e.g. fresh install with no stored value). Use .get() with safe defaults instead of hard dict access to avoid MultiValueDictKeyError.

…tings

Radio button groups (optimistic_mode, show_screensaver_inside/outside_temperature,
turn_on_behavior) are absent from POST data when neither option is pre-selected
(e.g. fresh install with no stored value). Use .get() with safe defaults instead
of hard dict access to avoid MultiValueDictKeyError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jamesmulcahy
Copy link
Copy Markdown
Contributor Author

After upgrading to the latest beta, I updated some settings in the UI, and hit save, and hit this error in Django, as it's assuming some fields are already set.

Environment:


Request Method: POST
Request URL: http://nspm.manque.net/save_settings

Django Version: 5.1.1
Python Version: 3.12.5
Installed Applications:
['web',
 'django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django_components.safer_staticfiles',
 'django_components']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/django/utils/datastructures.py", line 84, in __getitem__
    list_ = super().__getitem__(key)
            ^^^^^^^^^^^^^^^^^^^^^^^^

During handling of the above exception ('optimistic_mode'), another exception occurred:
  File "/usr/local/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
    response = get_response(request)
               ^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/django/core/handlers/base.py", line 197, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/src/app/nspanelmanager/web/views.py", line 772, in save_settings
    name="optimistic_mode", value=request.POST["optimistic_mode"] == "optimistic"
                                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/django/utils/datastructures.py", line 86, in __getitem__
    raise MultiValueDictKeyError(key)
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Exception Type: MultiValueDictKeyError at /save_settings
Exception Value: 'optimistic_mode'

Copy link
Copy Markdown
Collaborator

@tpanajott tpanajott left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a nice patch, thanks. Please see review for a few quick things before merging.

Comment thread docker/web/nspanelmanager/web/views.py Outdated
set_setting_value(
name="show_screensaver_inside_temperature",
value=request.POST["show_screensaver_inside_temperature"],
value=request.POST.get("show_screensaver_inside_temperature", "False"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to keep this to "True". We always want to show inside default as default.

Comment thread docker/web/nspanelmanager/web/views.py Outdated
set_setting_value(
name="show_screensaver_outside_temperature",
value=request.POST["show_screensaver_outside_temperature"],
value=request.POST.get("show_screensaver_outside_temperature", "False"),
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to keep this to "True". We always want to show outside default as default.

Comment thread docker/web/nspanelmanager/web/views.py Outdated
set_setting_value(
name="optimistic_mode", value=request.POST["optimistic_mode"] == "optimistic"
name="optimistic_mode",
value=request.POST.get("optimistic_mode", "") == "optimistic",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We want to keep optimistic mode as the default. So, adding "optimistic" to be the default in case it was not found would seem appropriate. Or perhaps even better, checking of "optimistic_mode" in request.POST before trying to access it as it would then not change a value in case it was missing in the request but actually set in the stored settings.

@tpanajott
Copy link
Copy Markdown
Collaborator

Having thought a bit more about this i find it strange that this issue would pop up. Sure it needs to be fixed but it shouldn't happen. When the manager first starts it will go through the database and remove any setting that exists but is no longer used and will set the default value in case it does not exist. This functionality can be seen here:

void MqttManagerConfig::populate_default_and_clean() {

It seems there may be some issue with the check in the Django template if it should check a radio box or not.

@jamesmulcahy
Copy link
Copy Markdown
Contributor Author

Having thought a bit more about this i find it strange that this issue would pop up. Sure it needs to be fixed but it shouldn't happen. When the manager first starts it will go through the database and remove any setting that exists but is no longer used and will set the default value in case it does not exist. This functionality can be seen here:

void MqttManagerConfig::populate_default_and_clean() {

It seems there may be some issue with the check in the Django template if it should check a radio box or not.

Thanks for calling this out -- I asked Claude to investigate and it seems to have found the issue. TL;DR is that there are two values stored in the written defaults, with different casings. This causes confusion. I've included the more detailed analysis from Claude below; and will push a PR shortly containing the fixes it proposed.


Optimistic Mode Bug — Root Cause & Fix Plan

PR

#366

What was reported

MultiValueDictKeyError: 'optimistic_mode' in save_settings on fresh installs /
after upgrade to the latest beta when the settings form was submitted.

The defensive fix (already merged to branch)

views.py was changed to use request.POST.get(key, default) instead of hard
request.POST[key] access for the four affected radio-button groups. This prevents
the crash but does not address the root cause.


Root cause 1 — Duplicate keys in _setting_key_map (C++)

mqtt_manager_config.hpp initialises an std::unordered_map with two entries each
for OPTIMISTIC_MODE and MQTT_WAIT_TIME:

Line Key DB name Default
186 OPTIMISTIC_MODE optimistic_mode "false"
187 MQTT_WAIT_TIME mqtt_wait_time "0"
230 MQTT_WAIT_TIME mqtt_wait_time "1000"
231 OPTIMISTIC_MODE optimistic_mode "True"

With unordered_map initialiser lists the first key wins; the second is silently
discarded. So populate_default_and_clean() writes "false" (not "True") and
"0" (not "1000") to the DB on a fresh install.

Confirmed intended defaults (per tpanajott PR review):

  • optimistic_mode"True" (optimistic mode ON by default)
  • mqtt_wait_time"1000"
  • show_screensaver_inside_temperature"True"
  • show_screensaver_outside_temperature"True"

Fix: Remove lines 186–187 (the erroneous first entries) so the correct entries
at 230–231 are no longer shadowed.


Root cause 2 — Case mismatch between C++ default and Django template

The C++ default writes "false" (lowercase). The Django template checks:

{% if optimistic_mode == "True" %}checked{% endif %}
{% if optimistic_mode == "False" %}checked{% endif %}

"false" matches neither condition, so no radio is pre-selected. A form submitted
with no radio selection omits that key from the POST body entirely — which is what
triggered the KeyError.

Django saves booleans via str(True/False) = "True"/"False" (title case), so
once the user saves successfully the value is correct. The problem only surfaces when
the C++ default is the only value ever stored.

Fix: Fixing root cause 1 (correct default "True") resolves this for optimistic_mode.
Additionally, make the template comparisons case-insensitive using the |lower filter
so any future casing drift is harmless.


Root cause 3 — Wrong .get() defaults in views.py

The existing branch fix used:

request.POST.get("show_screensaver_inside_temperature", "False")
request.POST.get("show_screensaver_outside_temperature", "False")

But per tpanajott's review, both should default to "True".

For optimistic_mode, tpanajott prefers skip the update entirely if the key is
absent from POST (rather than defaulting to a value), so the stored setting is not
accidentally overwritten.


Fixes summary

  1. mqtt_manager_config.hpp lines 186–187: Remove the duplicate/wrong entries so
    the correct defaults on lines 230–231 take effect.

  2. views.py save_settings: Fix .get() defaults for screensaver fields to "True";
    guard optimistic_mode with if "optimistic_mode" in request.POST.

  3. settings.html: Use |lower on template comparisons for optimistic_mode to
    be resilient to any future casing drift.

…tic_mode

The unordered_map had duplicate entries for OPTIMISTIC_MODE ("false") and
MQTT_WAIT_TIME ("0") earlier in the list, silently shadowing the correct
entries ("True" / "1000") that appeared later. Remove the early duplicates
so populate_default_and_clean() writes the intended defaults on fresh installs.

Also fix the screensaver temperature .get() fallbacks to "True" (matching the
C++ defaults and reviewer feedback), guard optimistic_mode with an `in` check
so a missing radio field never overwrites a stored value, and use the |lower
filter in the template comparisons to be resilient to future casing drift.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
// Map of SETTING key that points to a pair of two strings. Pair.first is database key as string, Pair.second is default value.
static inline std::unordered_map<MQTT_MANAGER_SETTING, std::pair<std::string, std::string>> _setting_key_map = {
{MQTT_MANAGER_SETTING::IS_HOME_ASSISTANT_ADDON, {"is_home_assistant_addon", "false"}},
{MQTT_MANAGER_SETTING::OPTIMISTIC_MODE, {"optimistic_mode", "false"}},
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are removed because they're duplicates -- they're also present further down

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants