forked from cardosofelipe/fast-next-template
Refactor locale validation and update style consistency across schemas, tests, and migrations
- Replaced `SUPPORTED_LOCALES` with `supported_locales` for naming consistency. - Applied formatting improvements to multiline statements for better readability. - Cleaned up redundant comments and streamlined test assertions.
This commit is contained in:
@@ -23,10 +23,7 @@ def upgrade() -> None:
|
|||||||
# VARCHAR(10) supports BCP 47 format (e.g., "en", "it", "en-US", "it-IT")
|
# VARCHAR(10) supports BCP 47 format (e.g., "en", "it", "en-US", "it-IT")
|
||||||
# Nullable: NULL means "not set yet", will use Accept-Language header fallback
|
# Nullable: NULL means "not set yet", will use Accept-Language header fallback
|
||||||
# Indexed: For analytics queries and filtering by locale
|
# Indexed: For analytics queries and filtering by locale
|
||||||
op.add_column(
|
op.add_column("users", sa.Column("locale", sa.String(length=10), nullable=True))
|
||||||
"users",
|
|
||||||
sa.Column("locale", sa.String(length=10), nullable=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create index on locale column for performance
|
# Create index on locale column for performance
|
||||||
op.create_index(
|
op.create_index(
|
||||||
|
|||||||
@@ -117,8 +117,9 @@ async def get_locale(
|
|||||||
if current_user and current_user.locale:
|
if current_user and current_user.locale:
|
||||||
# Validate that saved locale is still supported
|
# Validate that saved locale is still supported
|
||||||
# (in case SUPPORTED_LOCALES changed after user set preference)
|
# (in case SUPPORTED_LOCALES changed after user set preference)
|
||||||
if current_user.locale in SUPPORTED_LOCALES:
|
locale_value = str(current_user.locale)
|
||||||
return current_user.locale
|
if locale_value in SUPPORTED_LOCALES:
|
||||||
|
return locale_value
|
||||||
|
|
||||||
# Priority 2: Accept-Language header
|
# Priority 2: Accept-Language header
|
||||||
accept_language = request.headers.get("accept-language", "")
|
accept_language = request.headers.get("accept-language", "")
|
||||||
|
|||||||
@@ -40,9 +40,9 @@ class UserUpdate(BaseModel):
|
|||||||
locale: str | None = Field(
|
locale: str | None = Field(
|
||||||
None,
|
None,
|
||||||
max_length=10,
|
max_length=10,
|
||||||
pattern=r'^[a-z]{2}(-[A-Z]{2})?$',
|
pattern=r"^[a-z]{2}(-[A-Z]{2})?$",
|
||||||
description="User's preferred locale (BCP 47 format: en, it, en-US, it-IT)",
|
description="User's preferred locale (BCP 47 format: en, it, en-US, it-IT)",
|
||||||
examples=["en", "it", "en-US", "it-IT"]
|
examples=["en", "it", "en-US", "it-IT"],
|
||||||
)
|
)
|
||||||
is_active: bool | None = (
|
is_active: bool | None = (
|
||||||
None # Changed default from True to None to avoid unintended updates
|
None # Changed default from True to None to avoid unintended updates
|
||||||
@@ -70,12 +70,12 @@ class UserUpdate(BaseModel):
|
|||||||
return v
|
return v
|
||||||
# Only support English and Italian for template showcase
|
# Only support English and Italian for template showcase
|
||||||
# Note: Locales stored in lowercase for case-insensitive matching
|
# Note: Locales stored in lowercase for case-insensitive matching
|
||||||
SUPPORTED_LOCALES = {"en", "it", "en-us", "en-gb", "it-it"}
|
supported_locales = {"en", "it", "en-us", "en-gb", "it-it"}
|
||||||
# Normalize to lowercase for comparison and storage
|
# Normalize to lowercase for comparison and storage
|
||||||
v_lower = v.lower()
|
v_lower = v.lower()
|
||||||
if v_lower not in SUPPORTED_LOCALES:
|
if v_lower not in supported_locales:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported locale '{v}'. Supported locales: {sorted(SUPPORTED_LOCALES)}"
|
f"Unsupported locale '{v}'. Supported locales: {sorted(supported_locales)}"
|
||||||
)
|
)
|
||||||
# Return normalized lowercase version for consistency
|
# Return normalized lowercase version for consistency
|
||||||
return v_lower
|
return v_lower
|
||||||
|
|||||||
@@ -67,9 +67,7 @@ class TestParseAcceptLanguage:
|
|||||||
|
|
||||||
def test_parse_complex_header(self):
|
def test_parse_complex_header(self):
|
||||||
"""Test complex Accept-Language header with multiple locales"""
|
"""Test complex Accept-Language header with multiple locales"""
|
||||||
result = parse_accept_language(
|
result = parse_accept_language("it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6")
|
||||||
"it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7,fr;q=0.6"
|
|
||||||
)
|
|
||||||
assert result == "it-it"
|
assert result == "it-it"
|
||||||
|
|
||||||
def test_parse_whitespace_handling(self):
|
def test_parse_whitespace_handling(self):
|
||||||
@@ -199,9 +197,7 @@ class TestGetLocale:
|
|||||||
assert result == "en"
|
assert result == "en"
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_locale_from_accept_language_header(
|
async def test_locale_from_accept_language_header(self, async_user_without_locale):
|
||||||
self, async_user_without_locale
|
|
||||||
):
|
|
||||||
"""Test locale detection from Accept-Language header when user has no preference"""
|
"""Test locale detection from Accept-Language header when user has no preference"""
|
||||||
# Mock request with Italian Accept-Language (it-IT has highest priority)
|
# Mock request with Italian Accept-Language (it-IT has highest priority)
|
||||||
mock_request = MagicMock()
|
mock_request = MagicMock()
|
||||||
|
|||||||
@@ -334,11 +334,7 @@ class TestLocaleValidation:
|
|||||||
def test_locale_in_user_update_with_other_fields(self):
|
def test_locale_in_user_update_with_other_fields(self):
|
||||||
"""Test locale validation works when combined with other fields"""
|
"""Test locale validation works when combined with other fields"""
|
||||||
# Valid locale with other fields
|
# Valid locale with other fields
|
||||||
user = UserUpdate(
|
user = UserUpdate(first_name="Mario", last_name="Rossi", locale="it")
|
||||||
first_name="Mario",
|
|
||||||
last_name="Rossi",
|
|
||||||
locale="it"
|
|
||||||
)
|
|
||||||
assert user.locale == "it"
|
assert user.locale == "it"
|
||||||
assert user.first_name == "Mario"
|
assert user.first_name == "Mario"
|
||||||
|
|
||||||
@@ -347,7 +343,7 @@ class TestLocaleValidation:
|
|||||||
UserUpdate(
|
UserUpdate(
|
||||||
first_name="Pierre",
|
first_name="Pierre",
|
||||||
last_name="Dupont",
|
last_name="Dupont",
|
||||||
locale="fr" # Unsupported
|
locale="fr", # Unsupported
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_supported_locales_list(self):
|
def test_supported_locales_list(self):
|
||||||
@@ -357,7 +353,9 @@ class TestLocaleValidation:
|
|||||||
# Expected output (normalized to lowercase)
|
# Expected output (normalized to lowercase)
|
||||||
expected_outputs = ["en", "it", "en-us", "en-gb", "it-it"]
|
expected_outputs = ["en", "it", "en-us", "en-gb", "it-it"]
|
||||||
|
|
||||||
for input_locale, expected_output in zip(input_locales, expected_outputs):
|
for input_locale, expected_output in zip(
|
||||||
|
input_locales, expected_outputs, strict=True
|
||||||
|
):
|
||||||
user = UserUpdate(locale=input_locale)
|
user = UserUpdate(locale=input_locale)
|
||||||
assert user.locale == expected_output
|
assert user.locale == expected_output
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user