Compare commits
5 Commits
6dc3307e23
...
fix/issue-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed0d31bc04 | ||
| d413f6ce13 | |||
|
|
fee8504813 | ||
| 411e19e5d7 | |||
|
|
1c55fe26b9 |
@@ -56,6 +56,8 @@ class JsonFileRoomStorage:
|
|||||||
due_date_ts=b.get("due_date_ts"),
|
due_date_ts=b.get("due_date_ts"),
|
||||||
created_at=b["created_at"],
|
created_at=b["created_at"],
|
||||||
created_by_user_id=b["created_by_user_id"],
|
created_by_user_id=b["created_by_user_id"],
|
||||||
|
deleted_at=b.get("deleted_at"),
|
||||||
|
created_by_username=b.get("created_by_username"),
|
||||||
)
|
)
|
||||||
for b in data.get("bounties", [])
|
for b in data.get("bounties", [])
|
||||||
]
|
]
|
||||||
@@ -64,6 +66,8 @@ class JsonFileRoomStorage:
|
|||||||
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"),
|
||||||
|
admin_user_ids=data.get("admin_user_ids", []),
|
||||||
)
|
)
|
||||||
|
|
||||||
def save(self, room_data: RoomData) -> None:
|
def save(self, room_data: RoomData) -> None:
|
||||||
@@ -71,6 +75,8 @@ class JsonFileRoomStorage:
|
|||||||
data = {
|
data = {
|
||||||
"room_id": room_data.room_id,
|
"room_id": room_data.room_id,
|
||||||
"next_id": room_data.next_id,
|
"next_id": room_data.next_id,
|
||||||
|
"timezone": room_data.timezone,
|
||||||
|
"admin_user_ids": room_data.admin_user_ids or [],
|
||||||
"bounties": [
|
"bounties": [
|
||||||
{
|
{
|
||||||
"id": b.id,
|
"id": b.id,
|
||||||
@@ -79,6 +85,8 @@ class JsonFileRoomStorage:
|
|||||||
"due_date_ts": b.due_date_ts,
|
"due_date_ts": b.due_date_ts,
|
||||||
"created_at": b.created_at,
|
"created_at": b.created_at,
|
||||||
"created_by_user_id": b.created_by_user_id,
|
"created_by_user_id": b.created_by_user_id,
|
||||||
|
"deleted_at": b.deleted_at,
|
||||||
|
"created_by_username": b.created_by_username,
|
||||||
}
|
}
|
||||||
for b in room_data.bounties
|
for b in room_data.bounties
|
||||||
],
|
],
|
||||||
@@ -132,6 +140,35 @@ class JsonFileRoomStorage:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def list_bounties(self, room_id: int) -> list[Bounty]:
|
||||||
|
"""List all non-deleted bounties in a room.
|
||||||
|
|
||||||
|
This is the default method for normal queries - soft-deleted bounties
|
||||||
|
are excluded from results.
|
||||||
|
"""
|
||||||
|
room_data = self.load(room_id)
|
||||||
|
if room_data is None:
|
||||||
|
return []
|
||||||
|
return [b for b in room_data.bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
def list_all_bounties(
|
||||||
|
self, room_id: int, include_deleted: bool = True
|
||||||
|
) -> list[Bounty]:
|
||||||
|
"""List all bounties including or excluding soft-deleted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id: The room ID
|
||||||
|
include_deleted: If True, return all bounties including soft-deleted.
|
||||||
|
If False, return only non-deleted bounties.
|
||||||
|
Defaults to True for /recover functionality.
|
||||||
|
"""
|
||||||
|
room_data = self.load(room_id)
|
||||||
|
if room_data is None:
|
||||||
|
return []
|
||||||
|
if include_deleted:
|
||||||
|
return room_data.bounties
|
||||||
|
return [b for b in room_data.bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
|
||||||
class JsonFileTrackingStorage:
|
class JsonFileTrackingStorage:
|
||||||
"""TrackingStorage implementation using JSON files.
|
"""TrackingStorage implementation using JSON files.
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ class Bounty:
|
|||||||
|
|
||||||
The created_by_user_id field always refers to the user who created the bounty.
|
The created_by_user_id field always refers to the user who created the bounty.
|
||||||
It does NOT indicate whether the bounty is a group or personal bounty.
|
It does NOT indicate whether the bounty is a group or personal bounty.
|
||||||
|
|
||||||
|
The deleted_at field indicates soft-delete: None means not deleted,
|
||||||
|
a value means deleted at that Unix timestamp.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
@@ -17,6 +20,8 @@ class Bounty:
|
|||||||
due_date_ts: int | None
|
due_date_ts: int | None
|
||||||
created_at: int
|
created_at: int
|
||||||
created_by_user_id: int
|
created_by_user_id: int
|
||||||
|
deleted_at: int | None = None
|
||||||
|
created_by_username: str | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -37,11 +42,20 @@ class RoomData:
|
|||||||
|
|
||||||
The room_id can be negative for Telegram groups or positive for DMs.
|
The room_id can be negative for Telegram groups or positive for DMs.
|
||||||
The next_id field is used to generate unique bounty IDs within this room.
|
The next_id field is used to generate unique bounty IDs within this room.
|
||||||
|
|
||||||
|
The timezone field stores the room's timezone (e.g., "Asia/Jakarta"), default UTC+0.
|
||||||
|
The admin_user_ids field lists users who have admin privileges in this room.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
room_id: int
|
room_id: int
|
||||||
bounties: list[Bounty]
|
bounties: list[Bounty]
|
||||||
next_id: int
|
next_id: int
|
||||||
|
timezone: str | None = None
|
||||||
|
admin_user_ids: list[int] | None = None
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
if self.admin_user_ids is None:
|
||||||
|
self.admin_user_ids = []
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
|
|||||||
@@ -40,6 +40,25 @@ class RoomStorage(Protocol):
|
|||||||
"""Get a specific bounty from a room by ID."""
|
"""Get a specific bounty from a room by ID."""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
def list_bounties(self, room_id: int) -> list[Bounty]:
|
||||||
|
"""List all non-deleted bounties in a room.
|
||||||
|
|
||||||
|
Soft-deleted bounties (where deleted_at is not None) are excluded.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
def list_all_bounties(
|
||||||
|
self, room_id: int, include_deleted: bool = True
|
||||||
|
) -> list[Bounty]:
|
||||||
|
"""List all bounties including or excluding soft-deleted.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
room_id: The room ID
|
||||||
|
include_deleted: If True, return all bounties including soft-deleted.
|
||||||
|
If False, return only non-deleted bounties.
|
||||||
|
"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
@runtime_checkable
|
@runtime_checkable
|
||||||
class TrackingStorage(Protocol):
|
class TrackingStorage(Protocol):
|
||||||
|
|||||||
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
@@ -28,6 +28,22 @@ class TestBounty:
|
|||||||
assert b.due_date_ts == 1735689600
|
assert b.due_date_ts == 1735689600
|
||||||
assert b.created_at == 1735603200
|
assert b.created_at == 1735603200
|
||||||
assert b.created_by_user_id == 123
|
assert b.created_by_user_id == 123
|
||||||
|
assert b.deleted_at is None
|
||||||
|
assert b.created_by_username is None
|
||||||
|
|
||||||
|
def test_bounty_with_new_fields(self):
|
||||||
|
b = Bounty(
|
||||||
|
id=1,
|
||||||
|
text="Fix the bug",
|
||||||
|
link="https://github.com/example/repo/issues/1",
|
||||||
|
due_date_ts=1735689600,
|
||||||
|
created_at=1735603200,
|
||||||
|
created_by_user_id=123,
|
||||||
|
deleted_at=1736200000,
|
||||||
|
created_by_username="johndoe",
|
||||||
|
)
|
||||||
|
assert b.deleted_at == 1736200000
|
||||||
|
assert b.created_by_username == "johndoe"
|
||||||
|
|
||||||
def test_bounty_optional_fields_can_be_none(self):
|
def test_bounty_optional_fields_can_be_none(self):
|
||||||
b = Bounty(
|
b = Bounty(
|
||||||
@@ -41,6 +57,8 @@ class TestBounty:
|
|||||||
assert b.text is None
|
assert b.text is None
|
||||||
assert b.link is None
|
assert b.link is None
|
||||||
assert b.due_date_ts is None
|
assert b.due_date_ts is None
|
||||||
|
assert b.deleted_at is None
|
||||||
|
assert b.created_by_username is None
|
||||||
|
|
||||||
def test_bounty_comparison_equal(self):
|
def test_bounty_comparison_equal(self):
|
||||||
b1 = Bounty(
|
b1 = Bounty(
|
||||||
@@ -103,6 +121,8 @@ class TestRoomData:
|
|||||||
assert rd.room_id == -1001
|
assert rd.room_id == -1001
|
||||||
assert rd.bounties == []
|
assert rd.bounties == []
|
||||||
assert rd.next_id == 1
|
assert rd.next_id == 1
|
||||||
|
assert rd.timezone is None
|
||||||
|
assert rd.admin_user_ids == []
|
||||||
|
|
||||||
def test_create_dm_room_data(self):
|
def test_create_dm_room_data(self):
|
||||||
rd = RoomData(
|
rd = RoomData(
|
||||||
@@ -113,6 +133,8 @@ class TestRoomData:
|
|||||||
assert rd.room_id == 123456
|
assert rd.room_id == 123456
|
||||||
assert rd.bounties == []
|
assert rd.bounties == []
|
||||||
assert rd.next_id == 1
|
assert rd.next_id == 1
|
||||||
|
assert rd.timezone is None
|
||||||
|
assert rd.admin_user_ids == []
|
||||||
|
|
||||||
def test_room_data_with_bounties(self):
|
def test_room_data_with_bounties(self):
|
||||||
b = Bounty(
|
b = Bounty(
|
||||||
@@ -128,6 +150,25 @@ class TestRoomData:
|
|||||||
assert rd.bounties[0].text == "Task"
|
assert rd.bounties[0].text == "Task"
|
||||||
assert rd.bounties[0].created_by_user_id == 123
|
assert rd.bounties[0].created_by_user_id == 123
|
||||||
|
|
||||||
|
def test_room_data_with_new_fields(self):
|
||||||
|
rd = RoomData(
|
||||||
|
room_id=-1001,
|
||||||
|
bounties=[],
|
||||||
|
next_id=1,
|
||||||
|
timezone="Asia/Jakarta",
|
||||||
|
admin_user_ids=[123, 456],
|
||||||
|
)
|
||||||
|
assert rd.timezone == "Asia/Jakarta"
|
||||||
|
assert rd.admin_user_ids == [123, 456]
|
||||||
|
|
||||||
|
def test_room_data_admin_user_ids_defaults_to_empty_list(self):
|
||||||
|
rd = RoomData(
|
||||||
|
room_id=-1001,
|
||||||
|
bounties=[],
|
||||||
|
next_id=1,
|
||||||
|
)
|
||||||
|
assert rd.admin_user_ids == []
|
||||||
|
|
||||||
|
|
||||||
class TestTrackingData:
|
class TestTrackingData:
|
||||||
def test_create_tracking_data(self):
|
def test_create_tracking_data(self):
|
||||||
|
|||||||
@@ -48,6 +48,20 @@ class SimpleRoomStorage:
|
|||||||
return b
|
return b
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def list_bounties(self, room_id: int) -> list[Bounty]:
|
||||||
|
if room_id not in self._rooms:
|
||||||
|
return []
|
||||||
|
return [b for b in self._rooms[room_id].bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
def list_all_bounties(
|
||||||
|
self, room_id: int, include_deleted: bool = True
|
||||||
|
) -> list[Bounty]:
|
||||||
|
if room_id not in self._rooms:
|
||||||
|
return []
|
||||||
|
if include_deleted:
|
||||||
|
return self._rooms[room_id].bounties
|
||||||
|
return [b for b in self._rooms[room_id].bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
|
||||||
class SimpleTrackingStorage:
|
class SimpleTrackingStorage:
|
||||||
"""Minimal mock without ensure_tracking - tests if track_bounty works without it.
|
"""Minimal mock without ensure_tracking - tests if track_bounty works without it.
|
||||||
@@ -119,6 +133,20 @@ class MockRoomStorage:
|
|||||||
return b
|
return b
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def list_bounties(self, room_id: int) -> list[Bounty]:
|
||||||
|
if room_id not in self._rooms:
|
||||||
|
return []
|
||||||
|
return [b for b in self._rooms[room_id].bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
def list_all_bounties(
|
||||||
|
self, room_id: int, include_deleted: bool = True
|
||||||
|
) -> list[Bounty]:
|
||||||
|
if room_id not in self._rooms:
|
||||||
|
return []
|
||||||
|
if include_deleted:
|
||||||
|
return self._rooms[room_id].bounties
|
||||||
|
return [b for b in self._rooms[room_id].bounties if b.deleted_at is None]
|
||||||
|
|
||||||
|
|
||||||
class MockTrackingStorage:
|
class MockTrackingStorage:
|
||||||
"""Mock implementation of TrackingStorage for testing."""
|
"""Mock implementation of TrackingStorage for testing."""
|
||||||
|
|||||||
Reference in New Issue
Block a user