Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions widgets/explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from widgets.workflow_search import (
SearchFilters,
render_search_bar_with_redirect,
get_filtered_published_runs,
render_search_filters,
render_search_results,
)
Expand Down Expand Up @@ -62,14 +63,17 @@ def render(request: Request, search_filters: SearchFilters | None):
)

search_filters = search_filters or SearchFilters()
qs = get_filtered_published_runs(request.user, search_filters)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Result count is capped at 25 instead of showing total matches.

The result count will always show at most 25 because get_filtered_published_runs returns a sliced queryset (qs[:25]). When len(qs) is called on Line 76, it evaluates the sliced queryset and returns the number of items in the slice, not the total count of matching results.

For example, if 100 workflows match the filters, the UI will incorrectly display "25 results" instead of "100 results."

To fix this, obtain the count before slicing:

     search_filters = search_filters or SearchFilters()
-    qs = get_filtered_published_runs(request.user, search_filters)
+    # Get filtered queryset without slicing to obtain accurate count
+    from widgets.workflow_search import build_search_filter, build_workflow_access_filter, build_sort_filter
+    from bots.models import PublishedRun
+    
+    qs = PublishedRun.objects.all()
+    qs = build_search_filter(qs, search_filters, user=request.user)
+    qs = build_workflow_access_filter(qs, request.user)
+    qs = build_sort_filter(qs, search_filters)
+    result_count = qs.count()
+    qs = qs[:25]
+    
     render_search_bar_with_redirect(
         request=request,
         search_filters=search_filters,
         max_width="600px",
     )
     with gui.div(className="mt-3"):
         new_filters = render_search_filters(
             current_user=request.user,
             search_filters=copy(search_filters),
-            result_count=len(qs),
+            result_count=result_count,
         )

Alternatively, refactor get_filtered_published_runs to return both the count and the sliced queryset, or accept a parameter to control whether to apply the slice.

Also applies to: 76-76

🤖 Prompt for AI Agents
In widgets/explore.py around line 66, get_filtered_published_runs currently
returns a sliced queryset (qs[:25]) so calling len(qs) later yields the slice
length (<=25) instead of the total matches; fix by obtaining the total count
before slicing (e.g., call full_qs.count() or have get_filtered_published_runs
return both total_count and sliced_qs) and then use the sliced queryset for
display while showing the pre-slice count for the results summary;
alternatively, change get_filtered_published_runs to accept a flag to disable
slicing and call it twice (once for count, once for the page) or refactor it to
return (total_count, page_qs).

render_search_bar_with_redirect(
request=request,
search_filters=search_filters,
max_width="600px",
)
with gui.div(className="mt-3"):
new_filters = render_search_filters(
current_user=request.user, search_filters=copy(search_filters)
current_user=request.user,
search_filters=copy(search_filters),
result_count=len(qs),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Misleading Result Count Shows Limited Results

The result_count displays the count after the 25-item limit is applied rather than the total matching results. Since get_filtered_published_runs returns qs[:25], calling len(qs) evaluates only those 25 items. This means the UI will show "25 results" even when hundreds of workflows match the search filters, making the count misleading for users.

Fix in Cursor Fix in Web

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Unnecessary Database Queries Waste Resources

The database query executes unconditionally even when search_filters evaluates to False (empty filters). In this case, the query results from get_filtered_published_runs and len(qs) are never used since render_search_results is only called when search_filters is truthy (line 84). This wastes a database round-trip on every explore page visit without active filters.

Fix in Cursor Fix in Web

)
if new_filters != search_filters:
# if the search bar value has changed, redirect to the new search page
Expand All @@ -79,7 +83,7 @@ def render(request: Request, search_filters: SearchFilters | None):

if search_filters:
with gui.div(className="my-4"):
render_search_results(request.user, search_filters)
render_search_results(qs, request.user, search_filters)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Duplicate Database Queries

The database query executes twice when search filters are active: first via len(qs) on line 76 to get the count, then again on line 390 of workflow_search.py when select_related creates a new QuerySet that re-executes the query with joins. This double-execution could be avoided by computing the count differently or restructuring the flow to use a single query.

Fix in Cursor Fix in Web

return

for category, pages in all_home_pages_by_category.items():
Expand Down
16 changes: 12 additions & 4 deletions widgets/workflow_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
QuerySet,
Value,
)
from fastapi import Request
from django.utils.translation import ngettext
from pydantic import BaseModel, field_validator

from app_users.models import AppUser
Expand Down Expand Up @@ -80,7 +80,9 @@ def to_lower(cls, v):


def render_search_filters(
current_user: AppUser | None = None, search_filters: SearchFilters | None = None
current_user: AppUser | None = None,
search_filters: SearchFilters | None = None,
result_count: int | None = None,
):
if not search_filters:
search_filters = SearchFilters()
Expand Down Expand Up @@ -125,6 +127,11 @@ def render_search_filters(
with gui.div(
className=f"{col_class} d-flex gap-2 justify-content-end align-items-center",
):
if result_count is not None:
gui.caption(
f"{result_count} {ngettext('result', 'results', result_count)}",
className="text-muted d-none d-md-block",
)
with (
gui.styled(
"""
Expand Down Expand Up @@ -377,8 +384,9 @@ def _render_selectbox(
)


def render_search_results(user: AppUser | None, search_filters: SearchFilters):
qs = get_filtered_published_runs(user, search_filters)
def render_search_results(
qs: QuerySet[PublishedRun], user: AppUser | None, search_filters: SearchFilters
):
qs = qs.select_related("workspace", "created_by", "saved_run")

def _render_run(pr: PublishedRun):
Expand Down