Skip to content

autocomplete so far, needs html fixes#1186

Open
pelin-sayar wants to merge 8 commits intodevfrom
autocomplete
Open

autocomplete so far, needs html fixes#1186
pelin-sayar wants to merge 8 commits intodevfrom
autocomplete

Conversation

@pelin-sayar
Copy link
Collaborator

@pelin-sayar pelin-sayar commented Nov 2, 2025

GitHub Issues addressed

  • This PR closes

What I did

  • I made the backend for the autocomplete feature. I also wrote the js for it, but I'm having trouble with getting the HTML to display it on the website the way it should.

Screenshots

  • Before
  • After

Testing

  • A brief explanation of tests done/written or how reviewers can test your work

Questions/Discussions/Notes

Summary by CodeRabbit

  • New Features

    • Real-time grouped autocomplete for Courses, Instructors, and Clubs with click-to-fill-and-submit behavior and keyboard-agnostic focus handling.
    • New backend autocomplete API powering concise, ranked, limited results with graceful handling of empty or failed queries.
  • UI Updates

    • Streamlined search bar layout, integrated autocomplete dropdown, and updated styling for suggestions.
    • Hidden filter fields (weekdays, min GPA) and preserved search mode submitted via a hidden field.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 2, 2025

📝 Walkthrough

Walkthrough

Adds a typeahead autocomplete: new DRF serializers and an /api/autocomplete/ endpoint returning course, instructor, and club suggestions; client-side debounced autocomplete with AbortController; updated search bar template, hidden inputs, CSS, URL route, view signatures, and exports.

Changes

Cohort / File(s) Summary
Backend Serializers
tcf_website/api/serializers.py
Added CourseAutocompleteSerializer (id, title, number, subdepartment sourced from subdepartment.mnemonic), InstructorAutocompleteSerializer (id, full_name), and ClubAutocompleteSerializer (id, name, category_name sourced from category.name).
Autocomplete Endpoint & Search logic
tcf_website/views/search.py
Added autocomplete(request) DRF view returning serialized courses, instructors, and clubs; `fetch_clubs(query, limit: int
API Routing
tcf_website/urls.py
Added path("api/autocomplete/", views.autocomplete, name="autocomplete").
Frontend Autocomplete JS
tcf_website/static/search/autocomplete.js
New client-side autocomplete: debounced (250ms) input, AbortController cancellation, fetches /api/autocomplete/?q=...&mode=..., grouped rendering for Courses, Instructors, Clubs (up to 8 each), click-to-fill-and-submit, outside-click dismissal, and error handling.
Search Bar Template & Hidden Inputs
tcf_website/templates/search/searchbar.html
Reworked markup: added .searchbar-wrapper, id="search-input", inline mode toggle and submit, hidden fields weekdays (weekdays-input) and min_gpa (min-gpa-input), and injected autocomplete script and results container.
Styles
tcf_website/static/search/searchbar.css
Added .searchbar-wrapper and .autocomplete-suggestions UI styling with headers, item rows, positioning, shadow, and hover states.
View Exports
tcf_website/views/__init__.py
Updated exports to include autocomplete and deduplicated club_category import ordering; no runtime behavior change.
Mode Toggle Template
tcf_website/templates/club/mode_toggle.html
Added hidden input id="search-mode"/name="mode" to submit the current mode when using radio-mode; newline fix.

Sequence Diagram

sequenceDiagram
    autonumber
    participant User
    participant Browser
    participant API as /api/autocomplete/
    participant Views as views.search
    participant DB as Database/Index
    participant UI as Suggestions DOM

    User->>Browser: type query
    Browser->>Browser: debounce 250ms, abort previous via AbortController
    Browser->>API: GET /api/autocomplete/?q=...&mode=...
    API->>Views: call autocomplete view
    Views->>DB: query courses, instructors, clubs (with limits)
    DB-->>Views: matched results
    Views-->>API: serialized JSON (courses, instructors, clubs)
    API-->>Browser: JSON response
    Browser->>UI: render grouped suggestions
    User->>UI: click suggestion
    UI->>Browser: set input, clear suggestions, submit form
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • theClubForum #1117 — modifies tcf_website/api/serializers.py and tcf_website/views/search.py with club-related serializers and search/autocomplete logic (strong overlap).

Poem

🐇 I nibble on code with a bright little hop,
Debounced hops fetch names, clubs, and course crop,
Click a soft carrot, the input fills neat,
Suggestions then vanish—what a tidy feat!
Tiny paws clap for the autocomplete.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning The description is incomplete with missing issue reference, placeholder screenshots, and no testing information provided despite using the template structure. Fill in the GitHub issue number, add actual before/after screenshots, specify which tests were run or how to test, and complete the 'What I did' section with more detail.
Title check ❓ Inconclusive The title is vague and uses non-descriptive language ('so far', 'needs html fixes') that doesn't clearly convey the main change or feature being added. Revise the title to be more specific and clear, such as 'Add autocomplete API for courses, instructors, and clubs' or 'Implement autocomplete feature for search bar'.
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 84.62% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch autocomplete

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (3)
tcf_website/views/search.py (2)

348-396: Rename similarity_second to similarity_last for consistency.

Line 360 uses similarity_second for the last_name field, but the existing fetch_instructors function (line 215) uses similarity_last for the same purpose. This inconsistency reduces code readability.

Apply this diff:

     instructors = (
         Instructor.objects.annotate(
             similarity_first=TrigramSimilarity("first_name", query),
-            similarity_second=TrigramSimilarity("last_name", query),
+            similarity_last=TrigramSimilarity("last_name", query),
             similarity_full=TrigramSimilarity("full_name", query),
         )
         .annotate(
             max_similarity=Greatest(
                 F("similarity_first"),
-                F("similarity_second"),
+                F("similarity_last"),
                 F("similarity_full"),
                 output_field=FloatField(),
             )
         )

348-396: Consider extracting the instructor similarity logic to reduce duplication.

Both autocomplete and fetch_instructors (lines 207-238) compute instructor trigram similarities using nearly identical logic. This duplication increases maintenance burden.

Consider extracting a helper function like:

def _annotate_instructor_similarity(queryset, query):
    """Annotate instructor queryset with trigram similarity scores."""
    return queryset.annotate(
        similarity_first=TrigramSimilarity("first_name", query),
        similarity_last=TrigramSimilarity("last_name", query),
        similarity_full=TrigramSimilarity("full_name", query),
    ).annotate(
        max_similarity=Greatest(
            F("similarity_first"),
            F("similarity_last"),
            F("similarity_full"),
            output_field=FloatField(),
        )
    )

Then both functions can use: instructors = _annotate_instructor_similarity(Instructor.objects, query).filter(...)

tcf_website/templates/search/searchbar.html (1)

208-209: Fix inconsistent HTML attribute spacing.

Lines 208-209 use class = "..." and id = "..." with spaces around the equals sign, which is inconsistent with the rest of the file (e.g., lines 9, 10, 21 use class="..." without spaces).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 12e0ca1 and 55c057a.

📒 Files selected for processing (6)
  • tcf_website/api/serializers.py (2 hunks)
  • tcf_website/static/search/autocomplete.js (1 hunks)
  • tcf_website/templates/search/searchbar.html (1 hunks)
  • tcf_website/urls.py (2 hunks)
  • tcf_website/views/__init__.py (1 hunks)
  • tcf_website/views/search.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
tcf_website/api/serializers.py (1)
tcf_website/models/models.py (18)
  • Meta (151-160)
  • Meta (190-200)
  • Meta (242-249)
  • Meta (440-457)
  • Meta (513-521)
  • Meta (853-872)
  • Meta (905-908)
  • Meta (933-939)
  • Meta (981-992)
  • Meta (1043-1052)
  • Meta (1093-1099)
  • Meta (1347-1358)
  • Meta (1387-1397)
  • Meta (1597-1603)
  • Meta (1625-1635)
  • Meta (1657-1667)
  • Course (536-872)
  • Instructor (293-457)
tcf_website/templates/search/searchbar.html (2)
tcf_website/static/search/filters.js (2)
  • clearFilters (299-335)
  • saveFiltersToLocalStorage (77-106)
tcf_website/static/club/mode_toggle.js (1)
  • updateSearchbarState (25-47)
tcf_website/views/search.py (3)
tcf_website/static/search/autocomplete.js (2)
  • response (44-47)
  • data (54-54)
tcf_website/api/serializers.py (2)
  • CourseAutocompleteSerializer (155-164)
  • InstructorAutocompleteSerializer (175-183)
tcf_website/models/models.py (2)
  • Instructor (293-457)
  • Course (536-872)
tcf_website/urls.py (1)
tcf_website/views/search.py (2)
  • search (76-151)
  • autocomplete (349-396)
tcf_website/views/__init__.py (1)
tcf_website/views/browse.py (1)
  • club_category (548-584)
🪛 GitHub Actions: Continuous Integration
tcf_website/static/search/autocomplete.js

[error] 8-8: prettier/prettier Delete extraneous whitespace at line 8 (formatting issue detected by ESLint). Command failed: npx eslint -c .config/.eslintrc.yml tcf_website/static/

🪛 GitHub Check: eslint
tcf_website/static/search/autocomplete.js

[failure] 142-142:
Insert ,


[failure] 132-132:
Replace !suggestionsContainer.contains(event.target)·&&·event.target·!==·searchInput with ⏎······!suggestionsContainer.contains(event.target)·&&⏎······event.target·!==·searchInput⏎····


[failure] 46-46:
Insert ,


[failure] 31-31:
Insert ⏎·····


[failure] 17-17:
Delete ··


[failure] 8-8:
Delete ··

🪛 Ruff (0.14.2)
tcf_website/api/serializers.py

164-164: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


180-183: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🔇 Additional comments (7)
tcf_website/views/__init__.py (1)

10-10: LGTM!

The import reordering has no functional impact.

tcf_website/urls.py (1)

7-7: LGTM!

The autocomplete endpoint is properly configured with a clean URL pattern and named route.

Also applies to: 131-131

tcf_website/api/serializers.py (2)

155-164: LGTM!

The serializer correctly flattens the subdepartment relationship for a clean autocomplete response.


175-183: LGTM!

The serializer provides the minimal fields needed for instructor autocomplete.

tcf_website/templates/search/searchbar.html (3)

10-10: LGTM!

The id="search-input" attribute correctly enables the autocomplete JavaScript to target this input element.


204-205: LGTM!

The hidden inputs for weekdays and min_gpa are properly configured with appropriate default values and IDs that match the JavaScript in filters.js.


217-217: LGTM!

The autocomplete script is loaded after the DOM elements are in place, ensuring proper initialization.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tcf_website/views/search.py (1)

223-245: Add docstring for get_instructor_results to satisfy pylint

pylint is failing on this function (C0116: missing-function-docstring). Adding even a short descriptive docstring will clear the error and unblock CI.

-@api_view(["GET"])
-def autocomplete(request):
+@api_view(["GET"])
+def autocomplete(request):
@@
-def get_instructor_results(query, limit: int = 10):
+def get_instructor_results(query, limit: int = 10):
+    """Return instructors annotated with trigram similarity, limited to `limit` results."""
♻️ Duplicate comments (1)
tcf_website/static/search/autocomplete.js (1)

1-142: Blocking: fix lint/Prettier violations in autocomplete.js

CI is still red because Prettier/ESLint report whitespace and trailing-comma violations here (see failures at lines 8, 15, 43, 131, 141). Please run the project formatter (e.g., npx prettier --write tcf_website/static/search/autocomplete.js && npx eslint --fix tcf_website/static/search/autocomplete.js) before pushing again so the pipeline can pass.

🧹 Nitpick comments (1)
tcf_website/static/search/autocomplete.js (1)

82-118: Defensive guard before iterating suggestions

Right now we always call .forEach on data.courses/data.instructors, which will throw if either property is ever missing or null (for example, if the backend changes shape or a proxy strips fields). Wrapping the loops in the existing hasCourses/hasInstructors checks keeps the widget resilient.

-    data.courses.forEach((course) => {
-      const item = document.createElement("div");
-      item.classList.add("autocomplete-item");
-      item.style.cursor = "pointer";
-      item.textContent = `${course.subdepartment} ${course.number} — ${course.title}`;
-      item.addEventListener("click", () => {
-        searchInput.value = `${course.subdepartment} ${course.number}`;
-        clearSuggestions();
-        if (searchInput.form) {
-          searchInput.form.submit();
-        }
-      });
-      suggestionsContainer.appendChild(item);
-    });
+    if (hasCourses) {
+      data.courses.forEach((course) => {
+        const item = document.createElement("div");
+        item.classList.add("autocomplete-item");
+        item.style.cursor = "pointer";
+        item.textContent = `${course.subdepartment} ${course.number} — ${course.title}`;
+        item.addEventListener("click", () => {
+          searchInput.value = `${course.subdepartment} ${course.number}`;
+          clearSuggestions();
+          if (searchInput.form) {
+            searchInput.form.submit();
+          }
+        });
+        suggestionsContainer.appendChild(item);
+      });
+    }-    data.instructors.forEach((instructor) => {
-      const item = document.createElement("div");
-      item.classList.add("autocomplete-item");
-      item.style.cursor = "pointer";
-      item.textContent = instructor.full_name;
-      item.addEventListener("click", () => {
-        searchInput.value = instructor.full_name;
-        clearSuggestions();
-        if (searchInput.form) {
-          searchInput.form.submit();
-        }
-      });
-      suggestionsContainer.appendChild(item);
-    });
+    if (hasInstructors) {
+      data.instructors.forEach((instructor) => {
+        const item = document.createElement("div");
+        item.classList.add("autocomplete-item");
+        item.style.cursor = "pointer";
+        item.textContent = instructor.full_name;
+        item.addEventListener("click", () => {
+          searchInput.value = instructor.full_name;
+          clearSuggestions();
+          if (searchInput.form) {
+            searchInput.form.submit();
+          }
+        });
+        suggestionsContainer.appendChild(item);
+      });
+    }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55c057a and c1de925.

📒 Files selected for processing (2)
  • tcf_website/static/search/autocomplete.js (1 hunks)
  • tcf_website/views/search.py (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tcf_website/views/search.py (2)
tcf_website/static/search/autocomplete.js (2)
  • response (41-44)
  • data (51-51)
tcf_website/api/serializers.py (2)
  • CourseAutocompleteSerializer (155-164)
  • InstructorAutocompleteSerializer (175-183)
🪛 GitHub Actions: Continuous Integration
tcf_website/views/search.py

[error] 223-223: pylint: C0116: Missing function or method docstring (missing-function-docstring)


[error] 223-223: pylint reported a failure for the file due to missing docstring at the specified line.

tcf_website/static/search/autocomplete.js

[error] 8-8: Prettier formatting check failed. Run 'prettier --write' to fix code style issues in this file.

🪛 GitHub Check: eslint
tcf_website/static/search/autocomplete.js

[failure] 141-141:
Insert ,


[failure] 131-131:
Replace !suggestionsContainer.contains(event.target)·&&·event.target·!==·searchInput with ⏎······!suggestionsContainer.contains(event.target)·&&⏎······event.target·!==·searchInput⏎····


[failure] 43-43:
Insert ,


[failure] 15-15:
Delete ··


[failure] 8-8:
Delete ··

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
tcf_website/static/search/autocomplete.js (1)

15-145: Fix remaining ESLint/Prettier formatting violations so CI passes

ESLint/Prettier are still complaining (per hints) around:

  • Line 15: stray spaces on a blank line.
  • Line 43: missing trailing comma in the multi-line fetch call’s arguments.
  • Line 135: long conditional that Prettier wants split across lines.
  • Line 145: missing trailing comma in the multi-line addEventListener call.

The simplest fix is to run the project formatter on this file:

npx prettier --write tcf_website/static/search/autocomplete.js
# and/or, if configured:
npx eslint --fix tcf_website/static/search/autocomplete.js

Then commit the updated file so the CI lint step goes green.

🧹 Nitpick comments (1)
tcf_website/static/search/autocomplete.js (1)

9-14: Avoid silently disabling autocomplete when .searchbar-wrapper is missing

Right now, if searchInput.closest(".searchbar-wrapper") returns null, the script returns and autocomplete is completely disabled without any signal. That’s brittle if the HTML changes or if there are multiple search bars.

Consider either:

  • Falling back to searchInput.parentElement (or another known container), or
  • Logging a warning so it’s obvious in dev tools when the wrapper is missing.

That will make the feature more resilient to template changes.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c1de925 and af88366.

📒 Files selected for processing (3)
  • tcf_website/static/search/autocomplete.js (1 hunks)
  • tcf_website/static/search/searchbar.css (1 hunks)
  • tcf_website/templates/search/searchbar.html (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tcf_website/templates/search/searchbar.html
🧰 Additional context used
🪛 GitHub Actions: Continuous Integration
tcf_website/static/search/autocomplete.js

[error] 15-15: Prettier formatting check failed in tcf_website/static/search/autocomplete.js. Run 'prettier --write' to fix code style issues.

🪛 GitHub Check: eslint
tcf_website/static/search/autocomplete.js

[failure] 145-145:
Insert ,


[failure] 135-135:
Replace !suggestionsContainer.contains(event.target)·&&·event.target·!==·searchInput with ⏎······!suggestionsContainer.contains(event.target)·&&⏎······event.target·!==·searchInput⏎····


[failure] 43-43:
Insert ,


[failure] 15-15:
Delete ··

🔇 Additional comments (1)
tcf_website/static/search/autocomplete.js (1)

1-147: Autocomplete logic and error handling look solid

Nice job wiring this up: debounced input, AbortController-based cancellation, safe textContent usage, grouped headers, and outside-click handling all look correct and cohesive with the /api/autocomplete/ endpoint. No functional issues stand out in the core flow.

Comment on lines +306 to +360

/* Autocomplete suggestions positioning */
.searchbar-wrapper {
position: relative;
}

/* Remove focus outline from search input */
#search-input:focus {
outline: none !important;
box-shadow: none !important;
border-color: inherit;
}

.autocomplete-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 1000;
background: white;
border: 1px solid #e2e8f0;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-top: 0;
max-height: 400px;
overflow: hidden;
display: none;
padding: 0.5rem 0;
}

.autocomplete-header {
padding: 0.75rem 1rem;
font-weight: 600;
font-size: 0.875rem;
color: #4a5568;
background-color: #f7fafc;
border-bottom: 1px solid #e2e8f0;
margin: 0;
}

.autocomplete-item {
padding: 0.75rem 1rem;
cursor: pointer;
border-bottom: 1px solid #f7fafc;
margin: 0;
transition: background-color 0.15s ease;
}

.autocomplete-item:hover {
background-color: #f7fafc;
}

.autocomplete-item:last-child {
border-bottom: none;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Restore visible focus indication and allow dropdown scrolling

#search-input:focus removes both outline and box-shadow with !important, which effectively hides focus state for keyboard users and is an accessibility regression. Also, .autocomplete-suggestions uses overflow: hidden, so taller result sets will be clipped instead of scrollable.

Consider:

-/* Remove focus outline from search input */
-#search-input:focus {
-  outline: none !important;
-  box-shadow: none !important;
-  border-color: inherit;
-}
+/* Provide a visible, accessible focus style for the search input */
+#search-input:focus {
+  outline: 2px solid #d75626;
+  outline-offset: 2px;
+}

and:

-  max-height: 400px;
-  overflow: hidden;
+  max-height: 400px;
+  overflow-y: auto;

This keeps the custom look while preserving keyboard focus visibility and avoiding clipped suggestions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (3)
tcf_website/views/search.py (3)

207-221: Instructor search refactor looks good; consider relocating the threshold comment

Centralizing the instructor similarity query in get_instructor_results and letting fetch_instructors just map to simple dicts is clean and keeps the logic reusable for both the HTML search view and the API.

Minor nit: the # arbitrarily chosen threshold comment now lives in fetch_instructors, but the actual similarity_threshold = 0.5 is defined in get_instructor_results. Moving that comment down next to similarity_threshold (or removing it) would avoid confusion.

Also applies to: 223-246


248-260: Tighten limit handling and double‑check type‑hint compatibility

The normalize_search_query helper is a nice way to make CS3100 match the indexed CS 3100 pattern.

Two small suggestions around limit:

  1. if limit: will silently ignore limit=0 (treating it as “no limit”). Using an explicit is not None check is clearer and avoids that edge case:
-def fetch_courses(query, filters, limit: int | None = None):
+def fetch_courses(query, filters, limit: int | None = None):
@@
-    if limit:
+    if limit is not None:
         results = results[:limit]
  1. The int | None annotation requires Python 3.10+ unless you’re using from __future__ import annotations. If the project targets earlier versions, consider switching to Optional[int] instead (and importing it), or ensuring the runtime is 3.10+.

For example, if you want broader compatibility:

-from typing import Iterable
+from typing import Iterable, Optional
@@
-def fetch_courses(query, filters, limit: int | None = None):
+def fetch_courses(query, filters, limit: Optional[int] = None):
@@
-    if limit:
+    if limit is not None:
         results = results[:limit]

Also applies to: 295-297


352-370: Autocomplete endpoint wiring is correct; consider a minimum query length for load control

The DRF autocomplete view correctly:

  • Reads q from the query string.
  • Short‑circuits to empty results when there’s no input.
  • Uses the shared instructor/course query helpers.
  • Serializes with the new autocomplete serializers.

Given this will likely be called on each keypress, you may want to enforce a minimal query length (e.g., 2–3 characters) before hitting the database, to protect against unnecessary load from very short queries:

-    if not query:
-        return Response({"courses": [], "instructors": []})  # empty list if no input
+    # Avoid DB hits for empty or very short queries; adjust length threshold as needed.
+    if len(query) < 2:
+        return Response({"courses": [], "instructors": []})

This is optional, but can help in production traffic.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between af88366 and f3bc963.

📒 Files selected for processing (4)
  • tcf_website/static/search/autocomplete.js (1 hunks)
  • tcf_website/urls.py (2 hunks)
  • tcf_website/views/__init__.py (1 hunks)
  • tcf_website/views/search.py (5 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tcf_website/urls.py
  • tcf_website/static/search/autocomplete.js
🧰 Additional context used
🧬 Code graph analysis (2)
tcf_website/views/__init__.py (1)
tcf_website/views/browse.py (1)
  • club_category (498-534)
tcf_website/views/search.py (1)
tcf_website/api/serializers.py (2)
  • CourseAutocompleteSerializer (155-164)
  • InstructorAutocompleteSerializer (175-183)
🔇 Additional comments (2)
tcf_website/views/__init__.py (1)

7-15: Exposing club_category via the views package is consistent and safe

Adding club_category to the browse import block keeps the public tcf_website.views surface consistent with other views and removes duplication without affecting behavior. Change looks good.

tcf_website/views/search.py (1)

8-18: New paginator/DRF/serializer imports align with usage

The added imports (paginator classes, api_view, Response, and the autocomplete serializers) are all used below and match the patterns in the rest of the module. No issues here.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
tcf_website/static/search/autocomplete.js (1)

49-52: Use ES6 property shorthand for cleaner code.

The signal property can use ES6 shorthand syntax since the property name and variable name are identical.

🔎 Proposed fix
      const response = await fetch(
        `/api/autocomplete/?q=${encodeURIComponent(query)}&mode=${mode}`,
-        { signal: signal },
+        { signal },
      );
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f3bc963 and a0677da.

📒 Files selected for processing (7)
  • tcf_website/api/serializers.py
  • tcf_website/static/search/autocomplete.js
  • tcf_website/templates/club/mode_toggle.html
  • tcf_website/templates/search/searchbar.html
  • tcf_website/urls.py
  • tcf_website/views/__init__.py
  • tcf_website/views/search.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tcf_website/urls.py
  • tcf_website/views/init.py
🧰 Additional context used
🧬 Code graph analysis (3)
tcf_website/templates/club/mode_toggle.html (1)
tcf_website/static/club/mode_toggle.js (1)
  • updateSearchbarState (25-47)
tcf_website/api/serializers.py (1)
tcf_website/models/models.py (18)
  • Meta (151-160)
  • Meta (190-200)
  • Meta (242-249)
  • Meta (507-524)
  • Meta (580-588)
  • Meta (921-940)
  • Meta (963-966)
  • Meta (991-997)
  • Meta (1049-1060)
  • Meta (1111-1120)
  • Meta (1368-1379)
  • Meta (1408-1418)
  • Meta (1618-1624)
  • Meta (1646-1656)
  • Meta (1678-1688)
  • Course (603-940)
  • Instructor (293-524)
  • Club (219-249)
tcf_website/views/search.py (5)
tcf_website/static/search/autocomplete.js (6)
  • response (49-52)
  • instructors (84-84)
  • mode (46-46)
  • clubs (85-85)
  • data (59-59)
  • courses (83-83)
tcf_website/api/serializers.py (3)
  • CourseAutocompleteSerializer (155-164)
  • InstructorAutocompleteSerializer (175-183)
  • ClubAutocompleteSerializer (204-215)
tcf_website/static/search/filters.js (2)
  • filters (94-102)
  • filters (114-114)
tcf_website/views/browse.py (1)
  • parse_mode (150-153)
tcf_website/views/review.py (1)
  • parse_mode (23-26)
🪛 GitHub Actions: Continuous Integration
tcf_website/views/search.py

[error] 361-361: pylint: W0612 Unused variable 'mode' (unused-variable).

🪛 GitHub Check: eslint
tcf_website/static/search/autocomplete.js

[warning] 51-51:
Expected property shorthand

🪛 Ruff (0.14.10)
tcf_website/api/serializers.py

164-164: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


180-183: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

tcf_website/views/search.py

361-361: Unpacked variable mode is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🔇 Additional comments (6)
tcf_website/templates/search/searchbar.html (1)

5-212: LGTM! Searchbar restructuring properly supports autocomplete.

The searchbar has been successfully restructured to accommodate the autocomplete feature:

  • Added searchbar-wrapper container for proper positioning of the dropdown
  • Added id="search-input" for the autocomplete script hook
  • Integrated mode toggle and search button inline
  • Included autocomplete.js script reference

The layout changes align well with the autocomplete requirements and maintain proper form structure.

tcf_website/api/serializers.py (1)

155-215: LGTM! Autocomplete serializers are well-designed.

The three new autocomplete serializers are properly implemented:

  • CourseAutocompleteSerializer: Exposes minimal fields (id, title, number, subdepartment mnemonic) needed for autocomplete suggestions
  • InstructorAutocompleteSerializer: Returns id and full_name for instructor suggestions
  • ClubAutocompleteSerializer: Provides id, name, and category_name for club suggestions

The use of source parameter to access related model fields (subdepartment.mnemonic, category.name) is correct and efficient.

tcf_website/views/search.py (4)

155-196: LGTM! fetch_clubs properly implements limit parameter.

The addition of the limit parameter and its application after building the queryset is correct. The threshold-based filtering is preserved, and the limit is applied at the end as expected for autocomplete results.


212-250: LGTM! Instructor fetching refactored with shared helper.

The refactoring to extract get_instructor_results is a good improvement:

  • Reduces code duplication between fetch_instructors and the autocomplete endpoint
  • Maintains the same trigram similarity logic
  • Properly applies the limit parameter

253-303: LGTM! fetch_courses properly implements limit parameter.

The addition of the limit parameter is correctly applied after filtering and ordering, which ensures:

  • Search normalization is still performed
  • Filters are applied before limiting
  • Similarity threshold is respected
  • Results are ordered by relevance before truncation

357-382: Verify autocomplete endpoint behavior and consider empty clubs response.

The autocomplete endpoint implementation looks solid overall. However, there's a minor inconsistency: when in club mode (line 368-370), the response only includes clubs, but the frontend autocomplete.js expects all three keys (courses, instructors, clubs) based on lines 73-75 and 82-84.

While the frontend uses optional chaining and defaults to empty arrays (data?.courses?.length > 0, data.courses || []), it would be more consistent to always return all three keys for predictable API behavior.

Consider updating the club mode response for consistency:

     if is_club:
         clubs = fetch_clubs(query, limit=5)
-        return Response({"clubs": ClubAutocompleteSerializer(clubs, many=True).data})
+        return Response({
+            "courses": [],
+            "instructors": [],
+            "clubs": ClubAutocompleteSerializer(clubs, many=True).data
+        })

Alternatively, verify that the frontend properly handles partial responses in all cases.



class ClubAutocompleteSerializer(serializers.ModelSerializer):
"""DEF Serializer for Club autocomplete"""
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix typo in docstring.

The docstring has "DEF Serializer" but should be "DRF Serializer" (Django REST Framework).

🔎 Proposed fix
-    """DEF Serializer for Club autocomplete"""
+    """DRF Serializer for Club autocomplete"""
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"""DEF Serializer for Club autocomplete"""
"""DRF Serializer for Club autocomplete"""
🤖 Prompt for AI Agents
In tcf_website/api/serializers.py around line 205, the docstring contains a typo
"DEF Serializer for Club autocomplete"; update the docstring to read "DRF
Serializer for Club autocomplete" to correctly reference Django REST Framework
and save the file.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
tcf_website/views/search.py (2)

155-197: Apply limit to queryset before materialization for better performance.

The limit is currently applied at line 195 after the queryset has been fully evaluated and converted to a list of dictionaries (lines 182-192). For autocomplete with limit=5, this means fetching and processing all matching clubs from the database, then discarding most of them.

🔎 Proposed refactor to apply limit earlier
 def fetch_clubs(query, limit: int | None = None):
     """Get club data using Django Trigram similarity."""
     threshold = 0.15
     if not query:
-        return list(
-            Club.objects.annotate(
+        qs = Club.objects.annotate(
                 max_similarity=Value(1.0, output_field=FloatField()),
                 category_slug=F("category__slug"),
                 category_name=F("category__name"),
-            ).values(
+            )
+        if limit:
+            qs = qs[:limit]
+        return list(
+            qs.values(
                 "id",
                 "name",
                 "description",
                 "max_similarity",
                 "category_slug",
                 "category_name",
             )
         )
 
     qs = (
         Club.objects.annotate(sim=TrigramSimilarity("combined_name", query))
         .annotate(max_similarity=F("sim"))
         .filter(max_similarity__gte=threshold)
         .annotate(category_slug=F("category__slug"), category_name=F("category__name"))
         .order_by("-max_similarity")
     )
+    
+    if limit:
+        qs = qs[:limit]
 
     qs = [
         {
             "id": c.id,
             "name": c.name,
             "description": c.description,
             "max_similarity": c.max_similarity,
             "category_slug": c.category_slug,
             "category_name": c.category_name,
         }
         for c in qs
     ]
 
-    if limit:
-        qs = qs[:limit]
     return qs

357-382: Consider adding error handling for production robustness.

The autocomplete endpoint currently has no error handling for database exceptions, serialization errors, or other potential failures. While DRF provides some default error handling, explicitly catching and logging exceptions would improve debuggability and allow for more graceful degradation.

💡 Example error handling pattern
@api_view(["GET"])
def autocomplete(request):
    """implement autocomplete for search bar"""
    try:
        query = request.GET.get("q", "").strip()
        _, is_club = parse_mode(request)

        if not query:
            return Response(
                {"courses": [], "instructors": [], "clubs": []}
            )

        if is_club:
            clubs = fetch_clubs(query, limit=5)
            return Response({"clubs": ClubAutocompleteSerializer(clubs, many=True).data})

        instructors = get_instructor_results(query, limit=5)
        courses = fetch_courses(query, filters={}, limit=5)

        return Response(
            {
                "courses": CourseAutocompleteSerializer(courses, many=True).data,
                "instructors": InstructorAutocompleteSerializer(
                    instructors, many=True
                ).data,
            }
        )
    except Exception as e:
        # Log the error for debugging
        logger.exception("Autocomplete error for query: %s", query)
        # Return empty results rather than 500 error
        return Response(
            {"courses": [], "instructors": [], "clubs": []},
            status=200
        )

Note: You'll need to add import logging and logger = logging.getLogger(__name__) at the top of the file.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a0677da and 2b51f2a.

📒 Files selected for processing (1)
  • tcf_website/views/search.py
🔇 Additional comments (4)
tcf_website/views/search.py (4)

12-19: LGTM! DRF integration looks correct.

The imports for Django REST Framework decorators, Response, and the new autocomplete serializers are properly structured for the new API endpoint.


212-226: LGTM! Clean refactoring.

The extraction of query logic into get_instructor_results and the use of a dict comprehension with getattr makes the code more maintainable and testable.


228-250: LGTM! Efficient implementation.

The use of .only() to fetch minimal fields and applying the limit via queryset slicing ensures the database query includes a SQL LIMIT clause, making this efficient for autocomplete.


253-303: LGTM! Limit applied correctly.

The limit is applied to the queryset after filtering and ordering (lines 300-301), which ensures Django translates it to a SQL LIMIT clause for efficient autocomplete queries.

Comment on lines +357 to +382
@api_view(["GET"])
def autocomplete(request):
"""implement autocomplete for search bar"""
query = request.GET.get("q", "").strip()
_, is_club = parse_mode(request)

if not query:
return Response(
{"courses": [], "instructors": [], "clubs": []}
) # empty list if no input

if is_club:
clubs = fetch_clubs(query, limit=5)
return Response({"clubs": ClubAutocompleteSerializer(clubs, many=True).data})

instructors = get_instructor_results(query, limit=5)
courses = fetch_courses(query, filters={}, limit=5)

return Response(
{
"courses": CourseAutocompleteSerializer(courses, many=True).data,
"instructors": InstructorAutocompleteSerializer(
instructors, many=True
).data,
}
)
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix inconsistent response structure across modes.

The autocomplete endpoint returns different response structures depending on mode:

  • Empty query (lines 364-366): {"courses": [], "instructors": [], "clubs": []}
  • Club mode (line 370): {"clubs": [...]}
  • Course mode (lines 375-381): {"courses": [...], "instructors": [...]}

This inconsistency forces clients to handle different response shapes, complicating frontend logic and increasing the likelihood of bugs. For example, accessing response.courses in club mode would fail.

🔎 Proposed fix for consistent response structure
     if is_club:
         clubs = fetch_clubs(query, limit=5)
-        return Response({"clubs": ClubAutocompleteSerializer(clubs, many=True).data})
+        return Response({
+            "courses": [],
+            "instructors": [],
+            "clubs": ClubAutocompleteSerializer(clubs, many=True).data
+        })
 
     instructors = get_instructor_results(query, limit=5)
     courses = fetch_courses(query, filters={}, limit=5)
 
     return Response(
         {
             "courses": CourseAutocompleteSerializer(courses, many=True).data,
             "instructors": InstructorAutocompleteSerializer(
                 instructors, many=True
             ).data,
+            "clubs": [],
         }
     )
🤖 Prompt for AI Agents
In tcf_website/views/search.py around lines 357 to 382, the autocomplete
endpoint returns different response shapes per mode; change it to always return
the same keys ("courses", "instructors", "clubs") so clients can rely on a
consistent structure. Specifically, when query is empty return {"courses": [],
"instructors": [], "clubs": []}; when is_club is True return {"clubs":
ClubAutocompleteSerializer(clubs, many=True).data, "courses": [], "instructors":
[]}; and when course/instructor mode return the current {"courses":
CourseAutocompleteSerializer(...).data, "instructors":
InstructorAutocompleteSerializer(...).data, "clubs": []}. Ensure serializers are
used for present lists and empty lists are plain [].

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
tcf_website/api/serializers.py (1)

205-205: Fix typo in docstring.

The docstring contains "DEF Serializer" but should be "DRF Serializer" (Django REST Framework).

🔎 Proposed fix
-    """DEF Serializer for Club autocomplete"""
+    """DRF Serializer for Club autocomplete"""
tcf_website/views/search.py (1)

357-382: Fix inconsistent response structure across modes.

The autocomplete endpoint returns different response structures depending on mode:

  • Empty query: {"courses": [], "instructors": [], "clubs": []}
  • Club mode: {"clubs": [...]}
  • Course mode: {"courses": [...], "instructors": [...]}

This inconsistency forces clients to handle different response shapes, complicating frontend logic and increasing the likelihood of bugs (e.g., accessing response.courses in club mode would fail).

🔎 Proposed fix for consistent response structure
     if is_club:
         clubs = fetch_clubs(query, limit=5)
-        return Response({"clubs": ClubAutocompleteSerializer(clubs, many=True).data})
+        return Response({
+            "courses": [],
+            "instructors": [],
+            "clubs": ClubAutocompleteSerializer(clubs, many=True).data
+        })
 
     instructors = get_instructor_results(query, limit=5)
     courses = fetch_courses(query, filters={}, limit=5)
 
     return Response(
         {
             "courses": CourseAutocompleteSerializer(courses, many=True).data,
             "instructors": InstructorAutocompleteSerializer(
                 instructors, many=True
             ).data,
+            "clubs": [],
         }
     )
🧹 Nitpick comments (1)
tcf_website/static/search/autocomplete.js (1)

49-52: Use ES6 property shorthand.

Line 51 can use ES6 property shorthand syntax for cleaner code.

🔎 Proposed refactor
       const response = await fetch(
         `/api/autocomplete/?q=${encodeURIComponent(query)}&mode=${mode}`,
-        { signal: signal },
+        { signal },
       );
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 53cd8c2 and e65d41f.

📒 Files selected for processing (8)
  • tcf_website/api/serializers.py
  • tcf_website/static/search/autocomplete.js
  • tcf_website/static/search/searchbar.css
  • tcf_website/templates/club/mode_toggle.html
  • tcf_website/templates/search/searchbar.html
  • tcf_website/urls.py
  • tcf_website/views/__init__.py
  • tcf_website/views/search.py
🚧 Files skipped from review as they are similar to previous changes (2)
  • tcf_website/static/search/searchbar.css
  • tcf_website/urls.py
🧰 Additional context used
🧬 Code graph analysis (3)
tcf_website/views/__init__.py (2)
tcf_website/views/browse.py (1)
  • club_category (498-534)
tcf_website/views/search.py (2)
  • search (77-152)
  • autocomplete (358-382)
tcf_website/views/search.py (3)
tcf_website/api/serializers.py (3)
  • CourseAutocompleteSerializer (155-164)
  • InstructorAutocompleteSerializer (175-183)
  • ClubAutocompleteSerializer (204-215)
tcf_website/views/browse.py (1)
  • parse_mode (150-153)
tcf_website/views/review.py (1)
  • parse_mode (23-26)
tcf_website/api/serializers.py (1)
tcf_website/models/models.py (18)
  • Meta (151-160)
  • Meta (190-200)
  • Meta (242-249)
  • Meta (507-524)
  • Meta (580-588)
  • Meta (921-940)
  • Meta (963-966)
  • Meta (991-997)
  • Meta (1049-1060)
  • Meta (1111-1120)
  • Meta (1368-1379)
  • Meta (1408-1418)
  • Meta (1618-1624)
  • Meta (1646-1656)
  • Meta (1678-1688)
  • Course (603-940)
  • Instructor (293-524)
  • Club (219-249)
🪛 GitHub Check: eslint
tcf_website/static/search/autocomplete.js

[warning] 51-51:
Expected property shorthand

🪛 Ruff (0.14.10)
tcf_website/api/serializers.py

164-164: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)


180-183: Mutable class attributes should be annotated with typing.ClassVar

(RUF012)

🔇 Additional comments (10)
tcf_website/templates/club/mode_toggle.html (1)

16-20: LGTM! Previous duplicate parameter issue resolved.

The hidden input now correctly uses only the id attribute without a name attribute, avoiding the duplicate form parameter issue while still providing JavaScript access to the current mode value.

tcf_website/views/__init__.py (1)

9-9: LGTM! Clean API surface expansion.

The new exports (club_category and autocomplete) correctly expose the newly added view functions for public use.

Also applies to: 42-42

tcf_website/templates/search/searchbar.html (1)

5-212: LGTM! Well-structured autocomplete integration.

The template changes properly support the new autocomplete feature by adding the necessary wrapper, input ID, hidden fields, and script inclusion while maintaining the existing filter functionality.

tcf_website/api/serializers.py (2)

155-165: LGTM! Correct serializer implementation.

The CourseAutocompleteSerializer appropriately exposes the minimal fields needed for autocomplete and correctly uses source="subdepartment.mnemonic" to access the related subdepartment's mnemonic.


175-184: LGTM! Simple and correct serializer.

The InstructorAutocompleteSerializer correctly exposes only the essential fields (id and full_name) needed for instructor autocomplete.

tcf_website/views/search.py (5)

12-19: LGTM! Clean import additions.

The new imports for DRF decorators, Response, and autocomplete serializers are properly structured to support the new API endpoint.


155-196: LGTM! Clean limit support added.

The fetch_clubs function correctly adds optional limit parameter while maintaining backward compatibility for existing callers.


212-226: LGTM! Good refactoring for code reuse.

The fetch_instructors refactor appropriately delegates to the new get_instructor_results helper, enabling code reuse for the autocomplete endpoint while maintaining the existing return structure.


228-251: LGTM! Well-implemented search helper.

The get_instructor_results helper correctly computes trigram similarity across multiple name fields, applies the limit efficiently in the queryset, and returns results suitable for both autocomplete and search use cases.


253-303: LGTM! Backward-compatible limit support.

The fetch_courses function properly adds optional limit parameter while preserving existing behavior for callers that don't specify a limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants