sessions / d78529efe274a869db0ce78c4ffc8191f07cf505

/home/powellc/src/code.unbl.ink/secstate/vrobbler

This commit has no recorded session.

diff

show whitespace

commit d78529efe274a869db0ce78c4ffc8191f07cf505Author: Colin Powell <colin@unbl.ink>Date:   Sat Jul 4 11:41:24 2026 -0400    [boardgames] Add genres and categoriesdiff --git a/PROJECT.org b/PROJECT.orgindex bb58191..99d908c 100644--- a/PROJECT.org+++ b/PROJECT.org@@ -619,7 +619,6 @@ The Edit log form should have from top to bottom:   - Expansion ids (which should a multi-select widget of expansions for this game)   - Location (which should be a drop down of BoardGameLocations for this user) -** TODO Board games should have genres extracted from family data :boardgames:metadata: ** TODO Fix bug in fetching expansions for board games :boardgames: :PROPERTIES: :ID:       17995312-e76e-4a50-b591-0eab78cb59ab@@ -643,6 +642,11 @@ The Edit log form should have from top to bottom: boardgamegeek.exceptions.BGGApiError: invalid data for game id: 242117 #+end_src +** TODO Board games should have genres extracted from family data :boardgames:metadata:+:PROPERTIES:+:ID:       7214b270-dccc-4b98-ac58-ff4f76c8cda9+:END:+ ** DONE Exclude some board games from auto-expansion imports :boardgames: :PROPERTIES: :ID:       51ffdf20-e732-4774-b781-c3501d26d46fdiff --git a/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py b/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.pynew file mode 100644index 0000000..ec98d3c--- /dev/null+++ b/vrobbler/apps/boardgames/management/commands/cleanup_boardgame_metadata.py@@ -0,0 +1,81 @@+import logging+import time++from django.core.management.base import BaseCommand++logger = logging.getLogger(__name__)+++class Command(BaseCommand):+    help = "Refresh board game metadata from BGG (categories→genres, families→tags)"++    def add_arguments(self, parser):+        parser.add_argument(+            "--commit",+            action="store_true",+            help="Persist changes to the database",+        )+        parser.add_argument(+            "--force",+            action="store_true",+            help="Update all games even if they already have a published_date",+        )+        parser.add_argument(+            "--batch-size",+            type=int,+            default=50,+            help="Number of games to process per batch (default: 50)",+        )+        parser.add_argument(+            "--sleep",+            type=float,+            default=1.0,+            help="Seconds to sleep between API calls (default: 1.0)",+        )++    def handle(self, *args, **options):+        from boardgames.models import BoardGame++        commit = options["commit"]+        force = options["force"]+        batch_size = options["batch_size"]+        sleep_secs = options["sleep"]++        qs = BoardGame.objects.exclude(bggeek_id__isnull=True).exclude(bggeek_id="")+        total = qs.count()+        self.stdout.write(f"Found {total} board games with BGG IDs")++        if not commit:+            self.stdout.write(+                "Dry run — no API calls will be made. Use --commit to run lookups."+            )+            return++        enriched = 0+        skipped = 0++        for batch_num, offset in enumerate(range(0, total, batch_size)):+            batch = qs[offset : offset + batch_size]+            for game in batch:+                try:+                    game.fix_metadata(force_update=force)+                    enriched += 1+                except Exception as e:+                    self.stdout.write(+                        self.style.WARNING(+                            f"  [SKIPPED] {game.title} (BGG {game.bggeek_id}): {e}"+                        )+                    )+                    skipped += 1+                time.sleep(sleep_secs)++            self.stdout.write(+                f"  Batch {batch_num + 1}: {offset + len(batch)}/{total} — "+                f"enriched: {enriched}, skipped: {skipped}"+            )++        self.stdout.write(+            f"\nResults:\n"+            f"  Games enriched: {enriched}\n"+            f"  Games skipped:  {skipped}"+        )diff --git a/vrobbler/apps/boardgames/models.py b/vrobbler/apps/boardgames/models.pyindex f0647b2..c3bd442 100644--- a/vrobbler/apps/boardgames/models.py+++ b/vrobbler/apps/boardgames/models.py@@ -351,8 +351,9 @@ class BoardGame(ScrobblableMixin):                 data.pop("max_players")              # Pop extra BGG metadata that isn't a model field+            categories = data.pop("categories", [])+            families = data.pop("families", [])             data.pop("mechanics", None)-            data.pop("categories", None)             data.pop("designers", None)             data.pop("publishers", None)             data.pop("publisher", None)@@ -368,6 +369,11 @@ class BoardGame(ScrobblableMixin):             ) = BoardGamePublisher.objects.get_or_create(name=publisher_name)             self.save() +            for cat in categories:+                self.genre.add(cat.strip())+            for fam in families:+                self.tags.add(fam.strip())+             # Go get cover image if the URL is present             if cover_url and not self.cover:                 self.save_image_from_url(cover_url)@@ -417,6 +423,7 @@ class BoardGame(ScrobblableMixin):         mechanics = bgg_data.pop("mechanics", [])         designers = bgg_data.pop("designers", [])         categories = bgg_data.pop("categories", [])+        families = bgg_data.pop("families", [])         publishers = bgg_data.pop("publishers", [])         publisher = bgg_data.pop("publisher", [])         cover_url = bgg_data.pop("cover_url") or ""@@ -459,6 +466,11 @@ class BoardGame(ScrobblableMixin):                 publisher, _ = BoardGamePublisher.objects.get_or_create(name=name)                 game.publishers.add(publisher) +        for cat in categories:+            game.genre.add(cat.strip())+        for fam in families:+            game.tags.add(fam.strip())+         if expansions and not game.skip_expansions:             if defer_expansions:                 from boardgames.tasks import fetch_board_game_expansionsdiff --git a/vrobbler/apps/boardgames/sources/bgg.py b/vrobbler/apps/boardgames/sources/bgg.pyindex 68d906b..a9ec453 100644--- a/vrobbler/apps/boardgames/sources/bgg.py+++ b/vrobbler/apps/boardgames/sources/bgg.py@@ -32,6 +32,7 @@ def lookup_boardgame_from_bgg(         )         game_dict["mechanics"] = game.mechanics         game_dict["categories"] = game.categories+        game_dict["families"] = game.families         game_dict["designers"] = game.designers         game_dict["publishers"] = game.publishers         if game.publishers:diff --git a/vrobbler/apps/boardgames/tests/test_models.py b/vrobbler/apps/boardgames/tests/test_models.pyindex 7dbb655..a4f3f6c 100644--- a/vrobbler/apps/boardgames/tests/test_models.py+++ b/vrobbler/apps/boardgames/tests/test_models.py@@ -83,6 +83,7 @@ def _mock_bgg_game(bggeek_id, title, expansions=None):         playingtime = 60         mechanics = []         categories = []+        families = []         designers = []         publishers = [] diff --git a/vrobbler/templates/boardgames/boardgame_detail.html b/vrobbler/templates/boardgames/boardgame_detail.htmlindex e86a89e..274f7f1 100644--- a/vrobbler/templates/boardgames/boardgame_detail.html+++ b/vrobbler/templates/boardgames/boardgame_detail.html@@ -46,6 +46,20 @@     <p>         <a href="{{object.start_url}}">Play again</a>     </p>+    {% if object.genre.all %}+    <p>Genres:+        {% for tag in object.genre.all %}+        <a href="{% url 'boardgames:boardgame_list' %}?genre={{ tag.name|urlencode }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>+        {% endfor %}+    </p>+    {% endif %}+    {% if object.tags.all %}+    <p>Tags:+        {% for tag in object.tags.all %}+        <a href="{% url 'boardgames:boardgame_list' %}?tag={{ tag.name|urlencode }}" class="badge bg-secondary text-decoration-none">{{ tag.name }}</a>+        {% endfor %}+    </p>+    {% endif %} </div> {% if charts %} <div class="row">