From 181276b22afe854c614ec9f1543b935a43faa8a8 Mon Sep 17 00:00:00 2001 From: Stefan 'hr' Berder Date: Tue, 23 Apr 2024 14:37:47 +0700 Subject: [PATCH] sort choices for more natural human use Sorting the choices in forms helps with a more natural selection process --- tests/test_choices.py | 20 +++++++++++++++----- tests/test_choices_display_option.py | 18 ++++++++++-------- timezone_field/choices.py | 26 ++++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 15 deletions(-) diff --git a/tests/test_choices.py b/tests/test_choices.py index e43c826..57ba3d5 100644 --- a/tests/test_choices.py +++ b/tests/test_choices.py @@ -37,12 +37,21 @@ def tzs3_names(): ] +@pytest.fixture +def tzs3_names_sorted(): + yield [ + "America/Argentina/Buenos_Aires", + "America/Los_Angeles", + "Europe/London", + ] + + @pytest.fixture def tzs3_standard_displays(): yield [ + "America/Argentina/Buenos Aires", "America/Los Angeles", "Europe/London", - "America/Argentina/Buenos Aires", ] @@ -104,10 +113,11 @@ def test_with_gmt_offset_transition_backward(use_pytz, utc_tzobj): assert with_gmt_offset(tz_names, now=after, use_pytz=use_pytz) == [("Europe/London", "GMT+00:00 Europe/London")] -def test_standard_using_timezone_names(tzs3_names, tzs3_standard_displays): - assert standard(tzs3_names) == list(zip(tzs3_names, tzs3_standard_displays)) +def test_standard_using_timezone_names(tzs3_names, tzs3_names_sorted, tzs3_standard_displays): + assert standard(tzs3_names) == list(zip(tzs3_names_sorted, tzs3_standard_displays)) -def test_standard_using_timezone_objects(tzs3_names, tzs3_standard_displays, to_tzobj): +def test_standard_using_timezone_objects(tzs3_names, tzs3_names_sorted, tzs3_standard_displays, to_tzobj): tzs3_objects = [to_tzobj(tz) for tz in tzs3_names] - assert standard(tzs3_objects) == list(zip(tzs3_objects, tzs3_standard_displays)) + tzs3_objects_sorted = [to_tzobj(tz) for tz in tzs3_names_sorted] + assert standard(tzs3_objects) == list(zip(tzs3_objects_sorted, tzs3_standard_displays)) diff --git a/tests/test_choices_display_option.py b/tests/test_choices_display_option.py index c563842..60e5fb1 100644 --- a/tests/test_choices_display_option.py +++ b/tests/test_choices_display_option.py @@ -1,3 +1,5 @@ +from collections import Counter + import pytest from django import forms from django.db import models @@ -104,7 +106,7 @@ def test_form_field_invalid_choices_display(use_pytz): def test_form_field_none(ChoicesDisplayForm, base_tzstrs): form = ChoicesDisplayForm() values, displays = zip(*form.fields["tz_none"].choices) - assert values == tuple(base_tzstrs) + assert sorted(values) == sorted(base_tzstrs) assert displays[values.index("America/Los_Angeles")] == "America/Los Angeles" assert displays[values.index("Asia/Kolkata")] == "Asia/Kolkata" @@ -136,10 +138,10 @@ def test_form_field_limited_none(ChoicesDisplayForm): def test_form_field_limited_standard(ChoicesDisplayForm): form = ChoicesDisplayForm() assert form.fields["tz_limited_standard"].choices == [ - ("Asia/Tokyo", "Asia/Tokyo"), - ("Asia/Dubai", "Asia/Dubai"), - ("America/Argentina/Buenos_Aires", "America/Argentina/Buenos Aires"), ("Africa/Nairobi", "Africa/Nairobi"), + ("America/Argentina/Buenos_Aires", "America/Argentina/Buenos Aires"), + ("Asia/Dubai", "Asia/Dubai"), + ("Asia/Tokyo", "Asia/Tokyo"), ] @@ -156,7 +158,7 @@ def test_form_field_limited_with_gmt_offset(ChoicesDisplayForm): def test_model_form_field_none(ChoicesDisplayModelForm, to_tzobj, base_tzobjs): form = ChoicesDisplayModelForm() values, displays = zip(*form.fields["tz_none"].choices) - assert values == ("",) + tuple(base_tzobjs) + assert Counter(values) == Counter(("",) + tuple(base_tzobjs)) assert displays[values.index(to_tzobj("America/Los_Angeles"))] == "America/Los Angeles" assert displays[values.index(to_tzobj("Asia/Kolkata"))] == "Asia/Kolkata" @@ -192,10 +194,10 @@ def test_moel_form_field_limited_standard(ChoicesDisplayModelForm, to_tzobj): form = ChoicesDisplayModelForm() assert form.fields["tz_limited_standard"].choices == [ ("", "---------"), - (to_tzobj("Asia/Tokyo"), "Asia/Tokyo"), - (to_tzobj("Asia/Dubai"), "Asia/Dubai"), - (to_tzobj("America/Argentina/Buenos_Aires"), "America/Argentina/Buenos Aires"), (to_tzobj("Africa/Nairobi"), "Africa/Nairobi"), + (to_tzobj("America/Argentina/Buenos_Aires"), "America/Argentina/Buenos Aires"), + (to_tzobj("Asia/Dubai"), "Asia/Dubai"), + (to_tzobj("Asia/Tokyo"), "Asia/Tokyo"), ] diff --git a/timezone_field/choices.py b/timezone_field/choices.py index 20a5dea..d386665 100644 --- a/timezone_field/choices.py +++ b/timezone_field/choices.py @@ -3,6 +3,28 @@ from timezone_field.backends import get_tz_backend +def normalize_standard(tztuple): + """Normalize timezone names by replacing special characters with space. + + For proper sorting, using spaces makes comparisons more consistent. + + :param str tztuple: tuple of timezone and representation + """ + return tztuple[1].translate(str.maketrans({"-": " ", "_": " "})) + + +def normalize_gmt(tztuple): + """Normalize timezone GMT names for sorting. + + For proper sorting, using GMT values as a positive or negative number. + + :param str tztuple: tuple of timezone and representation + """ + gmt = tztuple[1].split()[0] + cmp = gmt.replace("GMT", "").replace(":", "") + return int(cmp) + + def standard(timezones): """ Given a list of timezones (either strings of timezone objects), @@ -14,7 +36,7 @@ def standard(timezones): for tz in timezones: tz_str = str(tz) choices.append((tz, tz_str.replace("_", " "))) - return choices + return sorted(choices, key=normalize_standard) def with_gmt_offset(timezones, now=None, use_pytz=None): @@ -41,4 +63,4 @@ def with_gmt_offset(timezones, now=None, use_pytz=None): _choices.append((delta, tz, display)) _choices.sort(key=lambda x: x[0]) choices = [(one, two) for zero, one, two in _choices] - return choices + return sorted(choices, key=normalize_gmt)