From 66d2a9fb8678127524ba2cb1e47c5e6383e986f1 Mon Sep 17 00:00:00 2001 From: shokollm <270575765+shokollm@users.noreply.github.com> Date: Thu, 9 Apr 2026 10:23:33 +0000 Subject: [PATCH] feat: add Category model and update storage for categories Models & Storage layer for category feature (Issue #85): - Add Category dataclass to core/models.py - id (slug): lowercase alphabetic only - name: display name - created_at: Unix timestamp - deleted_at: soft delete timestamp (None if active) - Add category_ids field to Bounty dataclass - list[str] for multiple categories per bounty - Default empty list for backward compatibility - Add categories field to RoomData dataclass - list[Category] for room-level categories - Default empty list - Update JsonFileRoomStorage to serialize/deserialize: - Category fields (id, name, created_at, deleted_at) - Bounty.category_ids - RoomData.categories Backward compatible: existing data without categories works fine. --- adapters/storage/json_file.py | 24 +++++++++++++++++++++++- core/models.py | 23 ++++++++++++++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/adapters/storage/json_file.py b/adapters/storage/json_file.py index db989ee..555797a 100644 --- a/adapters/storage/json_file.py +++ b/adapters/storage/json_file.py @@ -11,7 +11,7 @@ import os import tempfile from pathlib import Path -from core.models import Bounty, RoomData, TrackingData, TrackedBounty +from core.models import Bounty, Category, RoomData, TrackingData, TrackedBounty class JsonFileRoomStorage: @@ -57,16 +57,28 @@ class JsonFileRoomStorage: created_by_user_id=b["created_by_user_id"], deleted_at=b.get("deleted_at"), created_by_username=b.get("created_by_username"), + category_ids=b.get("category_ids", []), ) for b in data.get("bounties", []) ] + categories = [ + Category( + id=c["id"], + name=c["name"], + created_at=c["created_at"], + deleted_at=c.get("deleted_at"), + ) + for c in data.get("categories", []) + ] + return RoomData( room_id=data["room_id"], bounties=bounties, next_id=data["next_id"], timezone=data.get("timezone"), admin_usernames=data.get("admin_usernames", []), + categories=categories, ) def save(self, room_data: RoomData) -> None: @@ -76,6 +88,15 @@ class JsonFileRoomStorage: "next_id": room_data.next_id, "timezone": room_data.timezone, "admin_usernames": room_data.admin_usernames or [], + "categories": [ + { + "id": c.id, + "name": c.name, + "created_at": c.created_at, + "deleted_at": c.deleted_at, + } + for c in room_data.categories + ], "bounties": [ { "id": b.id, @@ -86,6 +107,7 @@ class JsonFileRoomStorage: "created_by_user_id": b.created_by_user_id, "deleted_at": b.deleted_at, "created_by_username": b.created_by_username, + "category_ids": b.category_ids, } for b in room_data.bounties ], diff --git a/core/models.py b/core/models.py index 9dd8ef7..99dab42 100644 --- a/core/models.py +++ b/core/models.py @@ -1,6 +1,20 @@ """Domain dataclasses for JIGAIDO bounty tracker.""" -from dataclasses import dataclass +from dataclasses import dataclass, field + + +@dataclass +class Category: + """A category for organizing bounties in a room. + + Categories are per-room and support soft delete. + The id (slug) must be lowercase alphabetic only (e.g., "bug", "feature"). + """ + + id: str # slug: lowercase alphabetic only, e.g., "bug", "feature" + name: str # display name: e.g., "Bug", "Feature" + created_at: int + deleted_at: int | None = None # soft delete @dataclass @@ -12,6 +26,8 @@ class Bounty: The deleted_at field indicates soft-delete: None means not deleted, a value means deleted at that Unix timestamp. + + The category_ids field lists category slugs associated with this bounty. """ id: int @@ -22,6 +38,7 @@ class Bounty: created_by_user_id: int deleted_at: int | None = None created_by_username: str | None = None + category_ids: list[str] = field(default_factory=list) @dataclass @@ -45,6 +62,7 @@ class RoomData: The timezone field stores the room's timezone (e.g., "Asia/Jakarta"), default UTC+0. The admin_usernames field lists usernames who have admin privileges in this room. + The categories field contains all categories for organizing bounties in this room. """ room_id: int @@ -52,10 +70,13 @@ class RoomData: next_id: int timezone: str | None = None admin_usernames: list[str] | None = None + categories: list[Category] = field(default_factory=list) def __post_init__(self): if self.admin_usernames is None: self.admin_usernames = [] + if self.categories is None: + self.categories = [] @dataclass