Archived chats no longer show inside folders

The unfiled list has filtered archived chats since archiving existed
(api/pages.py). The folder branch went through the ORM relationship, which
filters nothing, so an archived chat kept appearing as long as it was
filed -- and the "Empty" check read the same unfiltered list, so a folder
holding only archived chats would have claimed to be empty while listing
them.

Fixed on the model rather than in the template, as `Folder.visible_chats`.
The loop and the empty check now cannot disagree, because there is one
list and the template binds it once. Ordering matches the unfiled list:
pinned first, then most recently touched.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jaroslav Beneš
2026-08-01 00:30:08 +02:00
parent 85f18e99b2
commit ec457debb3
3 changed files with 47 additions and 2 deletions
+18
View File
@@ -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"<Folder {self.name}>"
@@ -30,17 +30,22 @@
</div>
<div class="folder__contents" x-show="open" x-cloak>
{# 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 %}
<p class="nav-empty">Empty</p>
{% endif %}
</div>
+22
View File
@@ -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))