mirror of
https://github.com/bookwyrm-social/bookwyrm.git
synced 2024-11-25 19:11:09 +00:00
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:
commit
9c3e6384f8
6 changed files with 64 additions and 12 deletions
|
@ -43,6 +43,7 @@ def search(
|
||||||
min_confidence: float = 0,
|
min_confidence: float = 0,
|
||||||
filters: Optional[list[Any]] = None,
|
filters: Optional[list[Any]] = None,
|
||||||
return_first: bool = False,
|
return_first: bool = False,
|
||||||
|
books: Optional[QuerySet[models.Edition]] = None,
|
||||||
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
|
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
|
||||||
"""search your local database"""
|
"""search your local database"""
|
||||||
filters = filters or []
|
filters = filters or []
|
||||||
|
@ -54,13 +55,15 @@ def search(
|
||||||
# first, try searching unique identifiers
|
# first, try searching unique identifiers
|
||||||
# unique identifiers never have spaces, title/author usually do
|
# unique identifiers never have spaces, title/author usually do
|
||||||
if not " " in query:
|
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 there were no identifier results...
|
||||||
if not results:
|
if not results:
|
||||||
# then try searching title/author
|
# then try searching title/author
|
||||||
results = search_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
|
return results
|
||||||
|
|
||||||
|
@ -98,9 +101,17 @@ def format_search_result(search_result):
|
||||||
|
|
||||||
|
|
||||||
def search_identifiers(
|
def search_identifiers(
|
||||||
query, *filters, return_first=False
|
query,
|
||||||
|
*filters,
|
||||||
|
return_first=False,
|
||||||
|
books=None,
|
||||||
) -> Union[Optional[models.Edition], QuerySet[models.Edition]]:
|
) -> 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):
|
if connectors.maybe_isbn(query):
|
||||||
# Oh did you think the 'S' in ISBN stood for 'standard'?
|
# Oh did you think the 'S' in ISBN stood for 'standard'?
|
||||||
normalized_isbn = query.strip().upper().rjust(10, "0")
|
normalized_isbn = query.strip().upper().rjust(10, "0")
|
||||||
|
@ -111,7 +122,7 @@ def search_identifiers(
|
||||||
for f in models.Edition._meta.get_fields()
|
for f in models.Edition._meta.get_fields()
|
||||||
if hasattr(f, "deduplication_field") and f.deduplication_field
|
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))
|
*filters, reduce(operator.or_, (Q(**f) for f in or_filters))
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
@ -121,12 +132,17 @@ def search_identifiers(
|
||||||
|
|
||||||
|
|
||||||
def search_title_author(
|
def search_title_author(
|
||||||
query, min_confidence, *filters, return_first=False
|
query,
|
||||||
|
min_confidence,
|
||||||
|
*filters,
|
||||||
|
return_first=False,
|
||||||
|
books=None,
|
||||||
) -> QuerySet[models.Edition]:
|
) -> QuerySet[models.Edition]:
|
||||||
"""searches for title and author"""
|
"""searches for title and author"""
|
||||||
|
books = books or models.Edition.objects
|
||||||
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
query = SearchQuery(query, config="simple") | SearchQuery(query, config="english")
|
||||||
results = (
|
results = (
|
||||||
models.Edition.objects.filter(*filters, search_vector=query)
|
books.filter(*filters, search_vector=query)
|
||||||
.annotate(rank=SearchRank(F("search_vector"), query))
|
.annotate(rank=SearchRank(F("search_vector"), query))
|
||||||
.filter(rank__gt=min_confidence)
|
.filter(rank__gt=min_confidence)
|
||||||
.order_by("-rank")
|
.order_by("-rank")
|
||||||
|
|
|
@ -101,7 +101,6 @@
|
||||||
{% plural %}
|
{% plural %}
|
||||||
{{ formatted_count }} books
|
{{ formatted_count }} books
|
||||||
{% endblocktrans %}
|
{% endblocktrans %}
|
||||||
|
|
||||||
{% if books.has_other_pages %}
|
{% if books.has_other_pages %}
|
||||||
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
|
{% blocktrans trimmed with start=books.start_index end=books.end_index %}
|
||||||
(showing {{ start }}-{{ end }})
|
(showing {{ start }}-{{ end }})
|
||||||
|
@ -111,6 +110,8 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</h2>
|
</h2>
|
||||||
|
{% include 'shelf/shelves_filters.html' with user=user query=query %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% if is_self and shelf.id %}
|
{% if is_self and shelf.id %}
|
||||||
<div class="column is-narrow">
|
<div class="column is-narrow">
|
||||||
|
@ -209,7 +210,17 @@
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
9
bookwyrm/templates/shelf/shelves_filter_field.html
Normal file
9
bookwyrm/templates/shelf/shelves_filter_field.html
Normal 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 %}
|
5
bookwyrm/templates/shelf/shelves_filters.html
Normal file
5
bookwyrm/templates/shelf/shelves_filters.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'snippets/filters_panel/filters_panel.html' %}
|
||||||
|
|
||||||
|
{% block filter_fields %}
|
||||||
|
{% include 'shelf/shelves_filter_field.html' %}
|
||||||
|
{% endblock %}
|
|
@ -51,7 +51,7 @@ class Search(View):
|
||||||
def api_book_search(request):
|
def api_book_search(request):
|
||||||
"""Return books via API response"""
|
"""Return books via API response"""
|
||||||
query = request.GET.get("q")
|
query = request.GET.get("q")
|
||||||
query = isbn_check(query)
|
query = isbn_check_and_format(query)
|
||||||
min_confidence = request.GET.get("min_confidence", 0)
|
min_confidence = request.GET.get("min_confidence", 0)
|
||||||
# only return local book results via json so we don't cascade
|
# only return local book results via json so we don't cascade
|
||||||
book_results = search(query, min_confidence=min_confidence)
|
book_results = search(query, min_confidence=min_confidence)
|
||||||
|
@ -64,7 +64,7 @@ def book_search(request):
|
||||||
"""the real business is elsewhere"""
|
"""the real business is elsewhere"""
|
||||||
query = request.GET.get("q")
|
query = request.GET.get("q")
|
||||||
# check if query is isbn
|
# check if query is isbn
|
||||||
query = isbn_check(query)
|
query = isbn_check_and_format(query)
|
||||||
min_confidence = request.GET.get("min_confidence", 0)
|
min_confidence = request.GET.get("min_confidence", 0)
|
||||||
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
|
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)
|
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"""
|
"""isbn10 or isbn13 check, if so remove separators"""
|
||||||
if query:
|
if query:
|
||||||
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
|
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
|
||||||
|
|
|
@ -15,12 +15,14 @@ from bookwyrm import forms, models
|
||||||
from bookwyrm.activitypub import ActivitypubResponse
|
from bookwyrm.activitypub import ActivitypubResponse
|
||||||
from bookwyrm.settings import PAGE_LENGTH
|
from bookwyrm.settings import PAGE_LENGTH
|
||||||
from bookwyrm.views.helpers import is_api_request, get_user_from_username
|
from bookwyrm.views.helpers import is_api_request, get_user_from_username
|
||||||
|
from bookwyrm.book_search import search
|
||||||
|
|
||||||
|
|
||||||
# pylint: disable=no-self-use
|
# pylint: disable=no-self-use
|
||||||
class Shelf(View):
|
class Shelf(View):
|
||||||
"""shelf page"""
|
"""shelf page"""
|
||||||
|
|
||||||
|
# pylint: disable=R0914
|
||||||
def get(self, request, username, shelf_identifier=None):
|
def get(self, request, username, shelf_identifier=None):
|
||||||
"""display a shelf"""
|
"""display a shelf"""
|
||||||
user = get_user_from_username(request.user, username)
|
user = get_user_from_username(request.user, username)
|
||||||
|
@ -32,6 +34,8 @@ class Shelf(View):
|
||||||
else:
|
else:
|
||||||
shelves = models.Shelf.privacy_filter(request.user).filter(user=user).all()
|
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
|
# get the shelf and make sure the logged in user should be able to see it
|
||||||
if shelf_identifier:
|
if shelf_identifier:
|
||||||
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
shelf = get_object_or_404(user.shelf_set, identifier=shelf_identifier)
|
||||||
|
@ -42,6 +46,7 @@ class Shelf(View):
|
||||||
FakeShelf = namedtuple(
|
FakeShelf = namedtuple(
|
||||||
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
"Shelf", ("identifier", "name", "user", "books", "privacy")
|
||||||
)
|
)
|
||||||
|
|
||||||
books = (
|
books = (
|
||||||
models.Edition.viewer_aware_objects(request.user)
|
models.Edition.viewer_aware_objects(request.user)
|
||||||
.filter(
|
.filter(
|
||||||
|
@ -50,6 +55,7 @@ class Shelf(View):
|
||||||
)
|
)
|
||||||
.distinct()
|
.distinct()
|
||||||
)
|
)
|
||||||
|
|
||||||
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
shelf = FakeShelf("all", _("All books"), user, books, "public")
|
||||||
|
|
||||||
if is_api_request(request) and shelf_identifier:
|
if is_api_request(request) and shelf_identifier:
|
||||||
|
@ -86,6 +92,9 @@ class Shelf(View):
|
||||||
|
|
||||||
books = sort_books(books, request.GET.get("sort"))
|
books = sort_books(books, request.GET.get("sort"))
|
||||||
|
|
||||||
|
if shelves_filter_query:
|
||||||
|
books = search(shelves_filter_query, books=books)
|
||||||
|
|
||||||
paginated = Paginator(
|
paginated = Paginator(
|
||||||
books,
|
books,
|
||||||
PAGE_LENGTH,
|
PAGE_LENGTH,
|
||||||
|
@ -103,6 +112,8 @@ class Shelf(View):
|
||||||
"page_range": paginated.get_elided_page_range(
|
"page_range": paginated.get_elided_page_range(
|
||||||
page.number, on_each_side=2, on_ends=1
|
page.number, on_each_side=2, on_ends=1
|
||||||
),
|
),
|
||||||
|
"shelves_filter_query": shelves_filter_query,
|
||||||
|
"size": "small",
|
||||||
}
|
}
|
||||||
|
|
||||||
return TemplateResponse(request, "shelf/shelf.html", data)
|
return TemplateResponse(request, "shelf/shelf.html", data)
|
||||||
|
|
Loading…
Reference in a new issue