docs: document shared search pattern for feed, gists, and project listings

Add a reusable `text_search_clause` helper in `database.py` that builds a SQLAlchemy `or_` of `ilike` clauses over specified columns, returning `None` when search is blank or the table lacks the columns. Wire it into `get_feed_posts`, `get_gists_list`, and the projects listing so all three public index pages support free-text search via a `search` query parameter. Introduce `_sidebar_search.html` as the single search-box partial, included at the top of each listing's left filter panel with `_action`, `_placeholder`, and `_hidden` locals to preserve active category/tab filters on submit. Expose the `search` field on `FeedOut`, `GistsOut`, and `ProjectsOut` API schemas with corresponding OpenAPI documentation. Update `CLAUDE.md` to note the rate-limiter exemption for `GET`/`HEAD` and the `/openai` gateway, and refresh `README.md` route tables to mention the new search capability on `/feed`, `/gists`, and `/projects`.
This commit is contained in:
2026-06-13 13:24:48 +00:00
parent 6756c980cf
commit 7985336934
15 changed files with 240 additions and 23 deletions
+78
View File
@@ -177,6 +177,84 @@ def test_feed_topic_filter_navigation(alice):
page.wait_for_url(f"{BASE_URL}/feed?topic=devlog", wait_until="domcontentloaded")
def _seed_searchable_posts():
token = uuid4().hex[:10]
users = get_table("users")
posts = get_table("posts")
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
match_title = f"Findable post {token}"
other_title = f"Unrelated post {uuid4().hex[:10]}"
for offset, title in ((0, match_title), (1, other_title)):
owner = str(uuid4())
users.insert(
{
"uid": owner,
"username": f"srch_{owner[:8]}",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
uid = str(uuid4())
posts.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"content": f"Body for {title}",
"topic": "devlog",
"project_uid": None,
"image": None,
"stars": 0,
"created_at": (base - timedelta(seconds=offset)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return token, match_title, other_title
def test_feed_search_bar_visible(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
search = page.locator("input[name='search'][placeholder='Search posts...']")
assert search.is_visible()
def test_feed_search_filters(alice):
page, user = alice
token, match_title, other_title = _seed_searchable_posts()
page.goto(f"{BASE_URL}/feed?search={token}", wait_until="domcontentloaded")
assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
assert page.locator(f".post-title:has-text('{other_title}')").count() == 0
def test_feed_search_no_match(alice):
page, user = alice
_seed_searchable_posts()
page.goto(
f"{BASE_URL}/feed?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
)
assert page.is_visible(".empty-state")
assert page.locator(".post-title").count() == 0
def test_feed_search_preserves_topic(alice):
page, user = alice
token, match_title, _ = _seed_searchable_posts()
page.goto(
f"{BASE_URL}/feed?topic=devlog&search={token}", wait_until="domcontentloaded"
)
assert page.locator(f".post-title:has-text('{match_title}')").count() == 1
preserved = page.locator(
".sidebar-card form input[type='hidden'][name='topic'][value='devlog']"
)
assert preserved.count() == 1
def test_feed_community_stats(alice):
page, user = alice
page.goto(f"{BASE_URL}/feed", wait_until="domcontentloaded")
+75
View File
@@ -261,6 +261,81 @@ def test_language_filter(alice, app_server):
assert not page.is_visible(f"text={title}")
def _seed_searchable_gists(language="python"):
token = uuid4().hex[:10]
owner = str(uuid4())
get_table("users").insert(
{
"uid": owner,
"username": f"gsrch_{owner[:8]}",
"email": f"{owner[:8]}@test.devplace",
"password_hash": "x",
"role": "Member",
"is_active": True,
"created_at": datetime.now(timezone.utc).isoformat(),
}
)
base = datetime(2026, 1, 1, tzinfo=timezone.utc)
gists = get_table("gists")
match_title = f"Findable gist {token}"
other_title = f"Unrelated gist {uuid4().hex[:10]}"
for offset, title in ((0, match_title), (1, other_title)):
uid = str(uuid4())
gists.insert(
{
"uid": uid,
"user_uid": owner,
"slug": make_combined_slug(title, uid),
"title": title,
"language": language,
"description": f"Description for {title}",
"source_code": "x = 1",
"stars": 0,
"created_at": (base - timedelta(seconds=offset)).isoformat(),
"deleted_at": None,
"deleted_by": None,
}
)
return token, match_title, other_title
def test_gists_search_bar_visible(alice):
page, _ = alice
page.goto(f"{BASE_URL}/gists", wait_until="domcontentloaded")
search = page.locator("input[name='search'][placeholder='Search gists...']")
assert search.is_visible()
def test_gists_search_filters(alice):
page, _ = alice
token, match_title, other_title = _seed_searchable_gists()
page.goto(f"{BASE_URL}/gists?search={token}", wait_until="domcontentloaded")
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
assert page.locator(f".gist-card-title:has-text('{other_title}')").count() == 0
def test_gists_search_no_match(alice):
page, _ = alice
_seed_searchable_gists()
page.goto(
f"{BASE_URL}/gists?search=zzz{uuid4().hex}", wait_until="domcontentloaded"
)
assert page.locator(".gist-card-title").count() == 0
def test_gists_search_preserves_language(alice):
page, _ = alice
token, match_title, _ = _seed_searchable_gists(language="go")
page.goto(
f"{BASE_URL}/gists?language=go&search={token}", wait_until="domcontentloaded"
)
assert page.locator(f".gist-card-title:has-text('{match_title}')").count() == 1
preserved = page.locator(
".sidebar-card form input[type='hidden'][name='language'][value='go']"
)
assert preserved.count() == 1
def test_gist_comments(alice, app_server):
page, _ = alice
title = f"Comment Test {int(time.time())}"