diff --git a/src/lembas/db/models/chat.py b/src/lembas/db/models/chat.py index 8d2a8d5..e4dca77 100644 --- a/src/lembas/db/models/chat.py +++ b/src/lembas/db/models/chat.py @@ -45,6 +45,24 @@ class Folder(UUIDPrimaryKey, Timestamps, Base): parent: Mapped[Folder | None] = relationship(back_populates="children", remote_side="Folder.id") chats: Mapped[list[Chat]] = relationship(back_populates="folder") + @property + def visible_chats(self) -> list[Chat]: + """The chats in this folder that belong in the sidebar. + + The relationship itself stays unfiltered -- back-population needs every + row -- so the listing rule lives here rather than in the template, where + the loop and the "Empty" check would have to agree by hand and already + did not: archived chats have been showing inside folders since folders + existed. The unfiled list has always filtered them (api/pages.py); the + folder branch went through the relationship and filtered nothing. + + Ordered like the unfiled list: pinned first, then most recently touched. + """ + kept = [chat for chat in self.chats if not chat.archived] + kept.sort(key=lambda chat: chat.updated_at, reverse=True) + kept.sort(key=lambda chat: not chat.pinned) + return kept + def __repr__(self) -> str: return f"" diff --git a/src/lembas/web/templates/partials/_folder.html b/src/lembas/web/templates/partials/_folder.html index a853d16..3c8ff9f 100644 --- a/src/lembas/web/templates/partials/_folder.html +++ b/src/lembas/web/templates/partials/_folder.html @@ -30,17 +30,22 @@
+ {# Bound once: the loop and the "Empty" check must be looking at the same + list, or a folder holding only archived chats claims to be empty while + showing them. #} + {% set listed = folder.visible_chats %} + {% for child in folder.children %} {% with folder = child %} {% include "partials/_folder.html" %} {% endwith %} {% endfor %} - {% for chat_item in folder.chats %} + {% for chat_item in listed %} {% include "partials/_chat_link.html" %} {% endfor %} - {% if not folder.children and not folder.chats %} + {% if not folder.children and not listed %} {% endif %}
diff --git a/tests/test_chat.py b/tests/test_chat.py index 22f6074..e23e01f 100644 --- a/tests/test_chat.py +++ b/tests/test_chat.py @@ -314,6 +314,28 @@ def test_deleting_a_folder_keeps_the_chats_inside_it(client: TestClient, db, reg assert chat.folder_id is None +def test_an_archived_chat_inside_a_folder_is_not_listed( + client: TestClient, db, registered, make_chat +): + """Regression: the unfiled list has always filtered archived chats, but the + folder branch went through the ORM relationship and filtered nothing, so an + archived chat kept showing as long as it was filed.""" + _add_connection(db) + client.post("/api/folders", data={"name": "Quests"}) + folder = db.scalar(select(Folder)) + + chat_id = make_chat() + client.patch(f"/api/chats/{chat_id}", data={"folder_id": folder.id, "title": "Mount Doom"}) + chat = db.get(Chat, chat_id) + chat.archived = True + db.commit() + + page = client.get("/chat").text + assert "Mount Doom" not in page + # And the folder must say so, rather than claiming to hold something. + assert "Empty" in page + + def test_a_folder_cannot_be_moved_inside_itself(client: TestClient, db, registered): client.post("/api/folders", data={"name": "Outer"}) folder = db.scalar(select(Folder))