Repository: mozilla/pontoon

Issue:
Cannot switch theme in Pontoon
**STR:**
1. Sign in to https://pontoon.mozilla.org/.
2. Click the user avatar in top right corner.
3. Under _Choose appearance_, click either _Light_ or _System_.

**Expected behavior:**
- Pontoon theme changes to the selected one.

**Actual behavior:**
- A red error message briefly shows up with the following text `{"status": false, "message": "Bad Request: Invalid theme"}`, which is the response body of the `POST https://pontoon.mozilla.org/api/v1/user/{some ID}/theme/` HTTP call.

![obrazek](https://github.com/mozilla/pontoon/assets/3941344/7434b566-fe88-4ba1-b238-0dadaaf8879f)

**NOTE 1:** The message shows really brifly, so it took me a while to take the screenshot successfully. Although I understand it disappear rather quickly because it hides actionable parts of the UI, it's still quite fast to be able to read it.

**NOTE 2:** The error message in the UI should be something human readable, not a JSON.


Discussion:
Thanks for filing, @MikkCZ! 

I haven't reproduced the issue yet, so I wonder if you could inspect that POST request in the Network tab of your developer tools. Specifically, it'd be interested to see what value is sent for the key "theme".
I tried in Firefox and Chromium, to make sure it's not something in the browser, but with same outcome. Here is HAR info of the request from my Firefox. I redacted the cookies and CSRF token values, and some other staff that look potentially sensitive:

```json
{
  "log": {
    "version": "1.2",
    "creator": {
      "name": "Firefox",
      "version": "122.0a1"
    },
    "browser": {
      "name": "Firefox",
      "version": "122.0a1"
    },
    "pages": [
      {
        "id": "page_1",
        "pageTimings": {
          "onContentLoad": -7985,
          "onLoad": -7638
        },
        "startedDateTime": "2023-12-03T14:19:36.144+01:00",
        "title": "https://pontoon.mozilla.org/cs/"
      }
    ],
    "entries": [
      {
        "startedDateTime": "2023-12-03T14:19:36.144+01:00",
        "request": {
          "bodySize": 96,
          "method": "POST",
          "url": "https://pontoon.mozilla.org/api/v1/user/jao8P-BzvE8uOS19Fx_iI89m8B8/theme/",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Host",
              "value": "pontoon.mozilla.org"
            },
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (X11; Linux x86_64; rv:122.0) Gecko/20100101 Firefox/122.0"
            },
            {
              "name": "Accept",
              "value": "*/*"
            },
            {
              "name": "Accept-Language",
              "value": "cs,sk;q=0.8,en-US;q=0.5,en;q=0.3"
            },
            {
              "name": "Accept-Encoding",
              "value": "gzip, deflate, br"
            },
            {
              "name": "Referer",
              "value": "https://pontoon.mozilla.org/cs/"
            },
            {
              "name": "Content-Type",
              "value": "application/x-www-form-urlencoded; charset=UTF-8"
            },
            {
              "name": "X-Requested-With",
              "value": "XMLHttpRequest"
            },
            {
              "name": "Content-Length",
              "value": "96"
            },
            {
              "name": "Origin",
              "value": "https://pontoon.mozilla.org"
            },
            {
              "name": "DNT",
              "value": "1"
            },
            {
              "name": "Sec-GPC",
              "value": "1"
            },
            {
              "name": "Connection",
              "value": "keep-alive"
            },
            {
              "name": "Cookie",
              "value": "<REDACTED>"
            },
            {
              "name": "Sec-Fetch-Dest",
              "value": "empty"
            },
            {
              "name": "Sec-Fetch-Mode",
              "value": "cors"
            },
            {
              "name": "Sec-Fetch-Site",
              "value": "same-origin"
            },
            {
              "name": "Pragma",
              "value": "no-cache"
            },
            {
              "name": "Cache-Control",
              "value": "no-cache"
            }
          ],
          "cookies": [
            "<REDACTED>"
          ],
          "queryString": [],
          "headersSize": 915,
          "postData": {
            "mimeType": "application/x-www-form-urlencoded",
            "params": [
              {
                "name": "csrfmiddlewaretoken",
                "value": "<REDACTED>"
              },
              {
                "name": "theme",
                "value": "light"
              }
            ],
            "text": "csrfmiddlewaretoken=<REDACTED>&theme=light"
          }
        },
        "response": {
          "status": 400,
          "statusText": "Bad Request",
          "httpVersion": "HTTP/1.1",
          "headers": [
            {
              "name": "Report-To",
              "value": "<REDACTED>"
            },
            {
              "name": "Reporting-Endpoints",
              "value": "<REDACTED>"
            },
            {
              "name": "Nel",
              "value": "{\"report_to\":\"heroku-nel\",\"max_age\":3600,\"success_fraction\":0.005,\"failure_fraction\":0.05,\"response_headers\":[\"Via\"]}"
            },
            {
              "name": "Connection",
              "value": "keep-alive"
            },
            {
              "name": "Server",
              "value": "gunicorn/19.9.0"
            },
            {
              "name": "Date",
              "value": "Sun, 03 Dec 2023 13:19:36 GMT"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            },
            {
              "name": "Content-Security-Policy",
              "value": "font-src 'self'; script-src 'self' 'unsafe-eval' 'sha256-fDsgbzHC0sNuBdM4W91nXVccgFLwIDkl197QEca/Cl4=' 'sha256-G5/M3dBlZdlvno5Cibw42fbeLr2PTEGd1M909Z7vPZE=' https://www.google-analytics.com/analytics.js; img-src 'self' https: data: https://*.wp.com/pontoon.mozilla.org/ https://www.google-analytics.com https://www.gravatar.com/avatar/; frame-src https:; worker-src https:; default-src 'none'; connect-src 'self' https://bugzilla.mozilla.org/rest/bug; style-src 'self' 'unsafe-inline'"
            },
            {
              "name": "X-Frame-Options",
              "value": "DENY"
            },
            {
              "name": "Vary",
              "value": "Cookie"
            },
            {
              "name": "Content-Length",
              "value": "58"
            },
            {
              "name": "X-Content-Type-Options",
              "value": "nosniff"
            },
            {
              "name": "X-Xss-Protection",
              "value": "1; mode=block"
            },
            {
              "name": "Referrer-Policy",
              "value": "same-origin"
            },
            {
              "name": "Strict-Transport-Security",
              "value": "max-age=31536000"
            },
            {
              "name": "Via",
              "value": "1.1 vegur"
            }
          ],
          "cookies": [],
          "content": {
            "mimeType": "application/json",
            "size": 58,
            "text": "{\"status\": false, \"message\": \"Bad Request: Invalid theme\"}"
          },
          "redirectURL": "",
          "headersSize": 1376,
          "bodySize": 1434
        },
        "cache": {},
        "timings": {
          "blocked": 0,
          "dns": 0,
          "connect": 0,
          "ssl": 0,
          "send": 0,
          "wait": 152,
          "receive": 0
        },
        "time": 152,
        "_securityState": "secure",
        "serverIPAddress": "52.202.168.65",
        "connection": "443",
        "pageref": "page_1"
      }
    ]
  }
}
```

The theme value is either `light` or `system`, depending on which button I click.
What I noticed now though is that the CSRF token included in that POST request is then used before in a previous POST to `https://pontoon.mozilla.org/log-ux-action/` (action type `Render:+Unread+notifications+icon`).

So although the error says the problem is the theme value, it might actually be the CSRF token?

Resolving patch:
diff --git a/pontoon/base/migrations/0052_rename_logged_in_users.py b/pontoon/base/migrations/0052_rename_logged_in_users.py
new file mode 100644
--- /dev/null
+++ b/pontoon/base/migrations/0052_rename_logged_in_users.py
@@ -0,0 +1,24 @@
+# Generated by Django 3.2.15 on 2023-12-05 20:02
+
+from django.db import migrations
+
+
+def rename_logged_in_users(apps, schema_editor):
+    UserProfile = apps.get_model("base", "UserProfile")
+    UserProfile.objects.filter(visibility_email="Logged in users").update(
+        visibility_email="Logged-in users"
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("base", "0051_localecodehistory"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            code=rename_logged_in_users,
+            reverse_code=migrations.RunPython.noop,
+        ),
+    ]
diff --git a/pontoon/contributors/views.py b/pontoon/contributors/views.py
--- a/pontoon/contributors/views.py
+++ b/pontoon/contributors/views.py
@@ -1,4 +1,5 @@
 import json
+import logging
 
 from dateutil.relativedelta import relativedelta
 from django.contrib import messages
@@ -26,6 +27,9 @@
 from pontoon.uxactionlog.utils import log_ux_action
 
 
+log = logging.getLogger(__name__)
+
+
 @login_required(redirect_field_name="", login_url="/403")
 def profile(request):
     """Current user profile."""
@@ -210,9 +214,10 @@ def toggle_theme(request, username):
         profile.theme = theme
         profile.full_clean()
         profile.save()
-    except ValidationError:
+    except ValidationError as e:
+        log.error(f"User profile validation error: {e}")
         return JsonResponse(
-            {"status": False, "message": "Bad Request: Invalid theme"},
+            {"status": False, "message": "Bad Request: User profile validation failed"},
             status=400,
         )
 


Test patch: