Merge pull request #84 from KucharczykL/fix/issue-43-game-dropdown-label
fix(game): show game name in dropdown labels (#43)
This commit is contained in:
@@ -114,6 +114,17 @@ def format_float_or_int(number: int | float):
|
||||
return int(number) if float(number).is_integer() else f"{number:03.2f}"
|
||||
|
||||
|
||||
def label_with_details(name: str, *details: object, separator: str = ", ") -> str:
|
||||
"""Build a ``"Name (detail, detail)"`` label from a name and optional details.
|
||||
|
||||
Falsy details (``None``, ``""``, ``0``) are dropped; the rest are stringified
|
||||
and joined with ``separator`` inside parentheses. With no details remaining,
|
||||
the bare ``name`` is returned without parentheses.
|
||||
"""
|
||||
present = [str(detail) for detail in details if detail]
|
||||
return f"{name} ({separator.join(present)})" if present else name
|
||||
|
||||
|
||||
OperatorType = Literal["|", "&"]
|
||||
|
||||
|
||||
|
||||
+8
-11
@@ -12,6 +12,7 @@ from django.template.defaultfilters import floatformat, pluralize, slugify
|
||||
from django.utils import timezone
|
||||
|
||||
from common.time import format_duration
|
||||
from common.utils import label_with_details
|
||||
|
||||
logger = logging.getLogger("games")
|
||||
|
||||
@@ -67,7 +68,7 @@ class Game(models.Model):
|
||||
|
||||
@property
|
||||
def search_label(self) -> str:
|
||||
return f"{self.sort_name} ({self.platform}, {self.year_released})"
|
||||
return label_with_details(self.name, self.platform, self.year_released)
|
||||
|
||||
def finished(self):
|
||||
return (
|
||||
@@ -234,16 +235,12 @@ class Purchase(models.Model):
|
||||
|
||||
@property
|
||||
def full_name(self):
|
||||
additional_info = [
|
||||
str(item)
|
||||
for item in [
|
||||
f"{self.num_purchases} game{pluralize(self.num_purchases)}",
|
||||
self.date_purchased,
|
||||
self.standardized_price,
|
||||
]
|
||||
if item
|
||||
]
|
||||
return f"{self.standardized_name} ({', '.join(additional_info)})"
|
||||
return label_with_details(
|
||||
self.standardized_name,
|
||||
f"{self.num_purchases} game{pluralize(self.num_purchases)}",
|
||||
self.date_purchased,
|
||||
self.standardized_price,
|
||||
)
|
||||
|
||||
def is_game(self):
|
||||
return self.type == self.GAME
|
||||
|
||||
@@ -63,6 +63,21 @@ class AddPurchasePricingTest(TestCase):
|
||||
{self.game_a.id, self.game_b.id},
|
||||
)
|
||||
|
||||
def test_full_name_keeps_parenthesized_detail_shape(self):
|
||||
bundle = Purchase.objects.create(
|
||||
name="Humble Bundle",
|
||||
date_purchased=date(2025, 1, 1),
|
||||
price=30,
|
||||
price_currency="USD",
|
||||
ownership_type=Purchase.DIGITAL,
|
||||
)
|
||||
bundle.games.set([self.game_a, self.game_b])
|
||||
bundle.refresh_from_db()
|
||||
|
||||
full_name = bundle.full_name
|
||||
self.assertTrue(full_name.startswith("Humble Bundle (2 games, "))
|
||||
self.assertTrue(full_name.endswith(")"))
|
||||
|
||||
|
||||
class SplitPurchaseTest(TestCase):
|
||||
def setUp(self):
|
||||
|
||||
@@ -338,6 +338,26 @@ class SearchLabelTest(django.test.TestCase):
|
||||
def test_format(self):
|
||||
self.assertEqual(self.game.search_label, "Mario (Steam, 2020)")
|
||||
|
||||
def test_format_uses_name_not_sort_name(self):
|
||||
game = Game.objects.create(
|
||||
name="Tetris", sort_name="", platform=self.platform, year_released=1984
|
||||
)
|
||||
self.assertEqual(game.search_label, "Tetris (Steam, 1984)")
|
||||
|
||||
def test_format_omits_missing_year(self):
|
||||
game = Game.objects.create(name="Tetris", platform=self.platform)
|
||||
self.assertEqual(game.search_label, "Tetris (Steam)")
|
||||
|
||||
def test_format_uses_sentinel_platform_when_unset(self):
|
||||
# Game.save() substitutes the "Unspecified" sentinel platform, so the
|
||||
# platform part is never a literal "None"; only year_released can drop out.
|
||||
game = Game.objects.create(name="Tetris", year_released=1984)
|
||||
self.assertEqual(game.search_label, "Tetris (Unspecified, 1984)")
|
||||
|
||||
def test_format_with_sentinel_platform_and_no_year(self):
|
||||
game = Game.objects.create(name="Tetris")
|
||||
self.assertEqual(game.search_label, "Tetris (Unspecified)")
|
||||
|
||||
def test_choice_fields_use_search_label(self):
|
||||
from games.forms import MultipleGameChoiceField, SingleGameChoiceField
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import unittest
|
||||
|
||||
from common.utils import label_with_details
|
||||
|
||||
|
||||
class LabelWithDetailsTest(unittest.TestCase):
|
||||
def test_all_parts_present(self):
|
||||
self.assertEqual(
|
||||
label_with_details("Mario", "Steam", 2020), "Mario (Steam, 2020)"
|
||||
)
|
||||
|
||||
def test_some_parts_falsy(self):
|
||||
self.assertEqual(label_with_details("Mario", None, 2020), "Mario (2020)")
|
||||
self.assertEqual(label_with_details("Mario", "Steam", None), "Mario (Steam)")
|
||||
|
||||
def test_all_parts_falsy(self):
|
||||
self.assertEqual(label_with_details("Mario", None, "", 0), "Mario")
|
||||
|
||||
def test_no_details(self):
|
||||
self.assertEqual(label_with_details("Mario"), "Mario")
|
||||
|
||||
def test_custom_separator(self):
|
||||
self.assertEqual(
|
||||
label_with_details("Mario", "a", "b", separator=" / "), "Mario (a / b)"
|
||||
)
|
||||
Reference in New Issue
Block a user