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

Let a user search books within their shelves
This commit is contained in:
Mouse Reeve 2024-01-02 18:37:33 -08:00 committed by GitHub
commit 9c3e6384f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 64 additions and 12 deletions

View file

@ -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")

View file

@ -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>

View file

@ -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 %}

View file

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

View file

@ -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)

View file

@ -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)