Compare commits

...

1 Commits

Author SHA1 Message Date
shokollm
66d2a9fb86 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.
2026-04-09 10:23:33 +00:00
2 changed files with 45 additions and 2 deletions

View File

@@ -11,7 +11,7 @@ import os
import tempfile import tempfile
from pathlib import Path from pathlib import Path
from core.models import Bounty, RoomData, TrackingData, TrackedBounty from core.models import Bounty, Category, RoomData, TrackingData, TrackedBounty
class JsonFileRoomStorage: class JsonFileRoomStorage:
@@ -57,16 +57,28 @@ class JsonFileRoomStorage:
created_by_user_id=b["created_by_user_id"], created_by_user_id=b["created_by_user_id"],
deleted_at=b.get("deleted_at"), deleted_at=b.get("deleted_at"),
created_by_username=b.get("created_by_username"), created_by_username=b.get("created_by_username"),
category_ids=b.get("category_ids", []),
) )
for b in data.get("bounties", []) 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( return RoomData(
room_id=data["room_id"], room_id=data["room_id"],
bounties=bounties, bounties=bounties,
next_id=data["next_id"], next_id=data["next_id"],
timezone=data.get("timezone"), timezone=data.get("timezone"),
admin_usernames=data.get("admin_usernames", []), admin_usernames=data.get("admin_usernames", []),
categories=categories,
) )
def save(self, room_data: RoomData) -> None: def save(self, room_data: RoomData) -> None:
@@ -76,6 +88,15 @@ class JsonFileRoomStorage:
"next_id": room_data.next_id, "next_id": room_data.next_id,
"timezone": room_data.timezone, "timezone": room_data.timezone,
"admin_usernames": room_data.admin_usernames or [], "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": [ "bounties": [
{ {
"id": b.id, "id": b.id,
@@ -86,6 +107,7 @@ class JsonFileRoomStorage:
"created_by_user_id": b.created_by_user_id, "created_by_user_id": b.created_by_user_id,
"deleted_at": b.deleted_at, "deleted_at": b.deleted_at,
"created_by_username": b.created_by_username, "created_by_username": b.created_by_username,
"category_ids": b.category_ids,
} }
for b in room_data.bounties for b in room_data.bounties
], ],

View File

@@ -1,6 +1,20 @@
"""Domain dataclasses for JIGAIDO bounty tracker.""" """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 @dataclass
@@ -12,6 +26,8 @@ class Bounty:
The deleted_at field indicates soft-delete: None means not deleted, The deleted_at field indicates soft-delete: None means not deleted,
a value means deleted at that Unix timestamp. a value means deleted at that Unix timestamp.
The category_ids field lists category slugs associated with this bounty.
""" """
id: int id: int
@@ -22,6 +38,7 @@ class Bounty:
created_by_user_id: int created_by_user_id: int
deleted_at: int | None = None deleted_at: int | None = None
created_by_username: str | None = None created_by_username: str | None = None
category_ids: list[str] = field(default_factory=list)
@dataclass @dataclass
@@ -45,6 +62,7 @@ class RoomData:
The timezone field stores the room's timezone (e.g., "Asia/Jakarta"), default UTC+0. 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 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 room_id: int
@@ -52,10 +70,13 @@ class RoomData:
next_id: int next_id: int
timezone: str | None = None timezone: str | None = None
admin_usernames: list[str] | None = None admin_usernames: list[str] | None = None
categories: list[Category] = field(default_factory=list)
def __post_init__(self): def __post_init__(self):
if self.admin_usernames is None: if self.admin_usernames is None:
self.admin_usernames = [] self.admin_usernames = []
if self.categories is None:
self.categories = []
@dataclass @dataclass