Repository: wagtail/wagtail
File: wagtail/admin/utils.py (Python)

Code before:
    """
    Given a URL and a dictionary of query parameters,
    returns a new URL with those query parameters added or updated.

    If the value of a query parameter is None, that parameter will be removed from the URL.
    """

    scheme, netloc, path, query, fragment = urlsplit(url)
    querydict = parse_qs(query)
    querydict.update(params)
    querydict = {key: value for key, value in querydict.items() if value is not None}
    query = urlencode(querydict, doseq=True)
    return urlunsplit((scheme, netloc, path, query, fragment))


def get_keyboard_key_labels_from_request(request):
    """
    Returns an instance of SimpleNamespace based on the user's keyboard layout
    based on the User-Agent header in the request.

    These are intentionally not translated, as they are key labels that are assumed
    to be consistent across all languages.
    """

    user_agent = request.headers.get("User-Agent", "")
    is_mac_os = re.search(r"Mac|iPod|iPhone|iPad", user_agent)

    labels = {
        "ALT": "⌥" if is_mac_os else "Alt",
        "CMD": "⌘" if is_mac_os else "Ctrl",
        "CTRL": "^" if is_mac_os else "Ctrl",
        "DEL": "Delete",
        "ENTER": "Return" if is_mac_os else "Enter",
        "ESC": "Esc",
        "SHIFT": "Shift",
        "TAB": "Tab",
    }

    return SimpleNamespace(**labels)


Review comment (bug):
Let's add a unit test for this stand alone utility.
I know it's being tested indirectly via the template tag tests but pulling this out means we need to support a basic suite of tests on its own.

Maybe add this just at the **start** of the file `wagtail/admin/tests/test_keyboard_shortcuts.py`. This way, we can ensure that this utility works correctly on its own.

Here's a rough starting point below with some pointers.

```py
import json
import re

from django.test import TestCase
from django.test.client import Client
from django.urls import reverse

from wagtail.admin.utils import get_keyboard_key_labels_from_request
from wagtail.test.utils import WagtailTestUtils


class TestGetKeyboardKeyLabelsFromRequestUtil(TestCase):
    def test_get_keyboard_key_labels_for_default(self):
        # add a test for the 'default' case, in our code path this will mimic windows, but we want to ensure that things do not break in this case
       # Maybe generate a request without user agent, in theory that should not happen but better to validate this edge case & ensure that we do get a valid set of values back

    def test_get_keyboard_key_labels_for_mac_os(self):
        # maybe use RequestFactory if Client is too heady handed, Client sub-classes RequestFactory
        # See https://docs.djangoproject.com/en/5.1/topics/testing/advanced/#the-request-factory
        # Creates a client with a Mac user agent
        client = Client(
            headers={
                "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36"
            }
        )
        request = client.get(reverse("wagtailadmin_home")) # remember, we do not care about the response here, just the request object with the right headers
        key_labels = get_keyboard_key_labels_from_request(request)
        # assertions here - checking a few of the mac/windows differences

    def test_get_keyboard_key_labels_for_windows(self):
        #... another one here

```





Code after:

    try:
        return user.get_username()
    except AttributeError:
        # we were passed None or something else that isn't a valid user object; return
        # empty string to replicate the behaviour of {{ user.get_full_name|default:user.get_username }}
        return ""


def set_query_params(url: str, params: dict):
    """
    Given a URL and a dictionary of query parameters,
    returns a new URL with those query parameters added or updated.

    If the value of a query parameter is None, that parameter will be removed from the URL.
    """

    scheme, netloc, path, query, fragment = urlsplit(url)
    querydict = parse_qs(query)
    querydict.update(params)
    querydict = {key: value for key, value in querydict.items() if value is not None}
    query = urlencode(querydict, doseq=True)
    return urlunsplit((scheme, netloc, path, query, fragment))


def get_keyboard_key_labels_from_request(request):
    """
    Returns an instance of SimpleNamespace based on the user's keyboard layout
    based on the User-Agent header in the request.

    These are intentionally not translated, as they are key labels that are assumed
    to be consistent across all languages.
    """

    user_agent = request.headers.get("User-Agent", "")
    is_mac_os = re.search(r"Mac|iPod|iPhone|iPad", user_agent)

    labels = {
        "ALT": "⌥" if is_mac_os else "Alt",
        "CMD": "⌘" if is_mac_os else "Ctrl",
        "CTRL": "^" if is_mac_os else "Ctrl",
        "DEL": "Delete",
        "ENTER": "Return" if is_mac_os else "Enter",
        "ESC": "Esc",
        "MOD": "⌘" if is_mac_os else "Ctrl",
        "SHIFT": "Shift",
        "TAB": "Tab",
    }

    return SimpleNamespace(**labels)