ソースを参照

Merge pull request #3118 from rosschapman/let-a-user-search-within-their-books

Let a user search books within their shelves
Mouse Reeve 9 ヶ月 前
コミット
9c3e6384f8

+ 23 - 7
bookwyrm/book_search.py

@@ -43,6 +43,7 @@ def search(
     min_confidence: float = 0,
     filters: Optional[list[Any]] = None,
     return_first: bool = False,
+    books: Optional[QuerySet[models.Edition]] = None,
 ) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
     """search your local database"""
     filters = filters or []
@@ -54,13 +55,15 @@ def search(
     # first, try searching unique identifiers
     # unique identifiers never have spaces, title/author usually do
     if not " " in query:
-        results = search_identifiers(query, *filters, return_first=return_first)
+        results = search_identifiers(
+            query, *filters, return_first=return_first, books=books
+        )
 
     # if there were no identifier results...
     if not results:
         # then try searching title/author
         results = search_title_author(
-            query, min_confidence, *filters, return_first=return_first
+            query, min_confidence, *filters, return_first=return_first, books=books
         )
     return results
 
@@ -98,9 +101,17 @@ def format_search_result(search_result):
 
 
 def search_identifiers(
-    query, *filters, return_first=False
+    query,
+    *filters,
+    return_first=False,
+    books=None,
 ) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
-    """tries remote_id, isbn; defined as dedupe fields on the model"""
+    """search Editions by deduplication fields
+
+    Best for cases when we can assume someone is searching for an exact match on
+    commonly unique data identifiers like isbn or specific library ids.
+    """
+    books = books or models.Edition.objects
     if connectors.maybe_isbn(query):
         # Oh did you think the 'S' in ISBN stood for 'standard'?
         normalized_isbn = query.strip().upper().rjust(10, "0")
@@ -111,7 +122,7 @@ def search_identifiers(
         for f in models.Edition._meta.get_fields()
         if hasattr(f, "deduplication_field") and f.deduplication_field
     ]
-    results = models.Edition.objects.filter(
+    results = books.filter(
         *filters, reduce(operator.or_, (Q(**f) for f in or_filters))
     ).distinct()
 
@@ -121,12 +132,17 @@ def search_identifiers(
 
 
 def search_title_author(
-    query, min_confidence, *filters, return_first=False
+    query,
+    min_confidence,
+    *filters,
+    return_first=False,
+    books=None,
 ) -> QuerySet[models.Edition]:
     """searches for title and author"""
+    books = books or models.Edition.objects
     query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
     results = (
-        models.Edition.objects.filter(*filters, search_vector=query)
+        books.filter(*filters, search_vector=query)
         .annotate(rank=SearchRank(F("search_vector"), query))
         .filter(rank__gt=min_confidence)
         .order_by("-rank")

+ 13 - 2
bookwyrm/templates/shelf/shelf.html

@@ -101,7 +101,6 @@
                     {% plural %}
                         {{ formatted_count }} books
                     {% endblocktrans %}
-
                     {% if books.has_other_pages %}
                         {% blocktrans trimmed with start=books.start_index end=books.end_index %}
                         (showing {{ start }}-{{ end }})
@@ -111,6 +110,8 @@
                 {% endif %}
                 {% endwith %}
             </h2>
+            {% include 'shelf/shelves_filters.html' with user=user query=query %}
+
         </div>
         {% if is_self and shelf.id %}
         <div class="column is-narrow">
@@ -209,7 +210,17 @@
             </tbody>
         </table>
         {% else %}
-        <p><em>{% trans "This shelf is empty." %}</em></p>
+        <p>
+            <em>
+                {% if shelves_filter_query %}
+                    {% blocktrans trimmed %}
+                        We couldn't find any books that matched {{ shelves_filter_query }}
+                    {% endblocktrans %}
+                {% else %}
+                    {% trans "This shelf is empty." %}
+                {% endif %}
+            </em>
+        </p>
         {% endif %}
 
     </div>

+ 9 - 0
bookwyrm/templates/shelf/shelves_filter_field.html

@@ -0,0 +1,9 @@
+{% extends 'snippets/filters_panel/filter_field.html' %}
+{% load i18n %}
+
+{% block filter %}
+  <div class="control">
+    <label class="label" for="filter_query">{% trans 'Filter by keyword' %}</label>
+    <input aria-label="Filter by keyword" id="my-books-filter" class="input" type="text" name="filter" placeholder="{% trans 'Enter text here' %}" value="{{ shelves_filter_query|default:'' }}" spellcheck="false" />
+  </div>
+{% endblock %}

+ 5 - 0
bookwyrm/templates/shelf/shelves_filters.html

@@ -0,0 +1,5 @@
+{% extends 'snippets/filters_panel/filters_panel.html' %}
+
+{% block filter_fields %}
+  {% include 'shelf/shelves_filter_field.html' %}
+{% endblock %}

+ 3 - 3
bookwyrm/views/search.py

@@ -51,7 +51,7 @@ class Search(View):
 def api_book_search(request):
     """Return books via API response"""
     query = request.GET.get("q")
-    query = isbn_check(query)
+    query = isbn_check_and_format(query)
     min_confidence = request.GET.get("min_confidence", 0)
     # only return local book results via json so we don't cascade
     book_results = search(query, min_confidence=min_confidence)
@@ -64,7 +64,7 @@ def book_search(request):
     """the real business is elsewhere"""
     query = request.GET.get("q")
     # check if query is isbn
-    query = isbn_check(query)
+    query = isbn_check_and_format(query)
     min_confidence = request.GET.get("min_confidence", 0)
     search_remote = request.GET.get("remote", False) and request.user.is_authenticated
 
@@ -159,7 +159,7 @@ def list_search(request):
     return TemplateResponse(request, "search/list.html", data)
 
 
-def isbn_check(query):
+def isbn_check_and_format(query):
     """isbn10 or isbn13 check, if so remove separators"""
     if query:
         su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)

+ 11 - 0
bookwyrm/views/shelf/shelf.py

@@ -15,12 +15,14 @@ from bookwyrm import forms, models
 from bookwyrm.activitypub import ActivitypubResponse
 from bookwyrm.settings import PAGE_LENGTH
 from bookwyrm.views.helpers import is_api_request, get_user_from_username
+from bookwyrm.book_search import search
 
 
 # pylint: disable=no-self-use
 class Shelf(View):
     """shelf page"""
 
+    # pylint: disable=R0914
     def get(self, request, username, shelf_identifier=None):
         """display a shelf"""
         user = get_user_from_username(request.user, username)
@@ -32,6 +34,8 @@ class Shelf(View):
         else:
             shelves = models.Shelf.privacy_filter(request.user).filter(user=user).all()
 
+        shelves_filter_query = request.GET.get("filter")
+
         # get the shelf and make sure the logged in user should be able to see it
         if shelf_identifier:
             shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
@@ -42,6 +46,7 @@ class Shelf(View):
             FakeShelf = namedtuple(
                 "Shelf", ("identifier", "name", "user", "books", "privacy")
             )
+
             books = (
                 models.Edition.viewer_aware_objects(request.user)
                 .filter(
@@ -50,6 +55,7 @@ class Shelf(View):
                 )
                 .distinct()
             )
+
             shelf = FakeShelf("all", _("All books"), user, books, "public")
 
         if is_api_request(request) and shelf_identifier:
@@ -86,6 +92,9 @@ class Shelf(View):
 
         books = sort_books(books, request.GET.get("sort"))
 
+        if shelves_filter_query:
+            books = search(shelves_filter_query, books=books)
+
         paginated = Paginator(
             books,
             PAGE_LENGTH,
@@ -103,6 +112,8 @@ class Shelf(View):
             "page_range": paginated.get_elided_page_range(
                 page.number, on_each_side=2, on_ends=1
             ),
+            "shelves_filter_query": shelves_filter_query,
+            "size": "small",
         }
 
         return TemplateResponse(request, "shelf/shelf.html", data)