bookwyrm/bookwyrm/views/search.py
2024-02-03 21:55:46 +01:00

219 lines
6.9 KiB
Python

""" search views"""
import re
from django.contrib.postgres.search import TrigramSimilarity
from django.core.paginator import Paginator
from django.db.models.functions import Greatest
from django.http import JsonResponse
from django.template.response import TemplateResponse
from django.views import View
from csp.decorators import csp_update
from bookwyrm import models
from bookwyrm.connectors import connector_manager
from bookwyrm.book_search import search, format_search_result
from bookwyrm.settings import PAGE_LENGTH, INSTANCE_ACTOR_USERNAME
from bookwyrm.utils import regex
from .helpers import is_api_request
from .helpers import handle_remote_webfinger
# pylint: disable= no-self-use
class Search(View):
"""search users or books"""
@csp_update(IMG_SRC="*")
def get(self, request):
"""that search bar up top"""
if is_api_request(request):
return api_book_search(request)
query = request.GET.get("q")
if not query:
return TemplateResponse(request, "search/book.html")
search_type = request.GET.get("type")
if query and not search_type:
search_type = "user" if "@" in query else "book"
endpoints = {
"book": book_search,
"author": author_search,
"user": user_search,
"list": list_search,
}
if not search_type in endpoints:
search_type = "book"
return endpoints[search_type](request)
def api_book_search(request):
"""Return books via API response"""
query = request.GET.get("q")
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)
return JsonResponse(
[format_search_result(r) for r in book_results[:10]], safe=False
)
def book_search(request):
"""the real business is elsewhere"""
query = request.GET.get("q")
# check if query is isbn
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
# try a local-only search
local_results = search(query, min_confidence=min_confidence)
paginated = Paginator(local_results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data = {
"query": query,
"results": page,
"type": "book",
"remote": search_remote,
"page_range": paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
),
}
# if a logged in user requested remote results or got no local results, try remote
if request.user.is_authenticated and (not local_results or search_remote):
data["remote_results"] = connector_manager.search(
query, min_confidence=min_confidence
)
data["remote"] = True
return TemplateResponse(request, "search/book.html", data)
def author_search(request):
"""search for an author"""
query = request.GET.get("q")
query = query.strip()
data = {"type": "author", "query": query}
results = (
models.Author.objects.annotate(
similarity=TrigramSimilarity("name", query),
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")
)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data["results"] = page
data["page_range"] = paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
)
return TemplateResponse(request, "search/author.html", data)
def user_search(request):
"""user search: search for a user"""
viewer = request.user
query = request.GET.get("q")
query = query.strip()
data = {"type": "user", "query": query}
# use webfinger for mastodon style account@domain.com username to load the user if
# they don't exist locally (handle_remote_webfinger will check the db)
if re.match(regex.FULL_USERNAME, query) and viewer.is_authenticated:
handle_remote_webfinger(query)
results = (
models.User.viewer_aware_objects(viewer)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
)
)
.filter(
similarity__gt=0.5,
)
.exclude(localname=INSTANCE_ACTOR_USERNAME)
.order_by("-similarity")
)
# don't expose remote users
if not viewer.is_authenticated:
results = results.filter(local=True)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data["results"] = page
data["page_range"] = paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
)
return TemplateResponse(request, "search/user.html", data)
def list_search(request):
"""any relevent lists?"""
query = request.GET.get("q")
data = {"query": query, "type": "list"}
results = (
models.List.privacy_filter(
request.user,
privacy_levels=["public", "followers"],
)
.annotate(
similarity=Greatest(
TrigramSimilarity("name", query),
TrigramSimilarity("description", query),
)
)
.filter(
similarity__gt=0.1,
)
.order_by("-similarity")
)
paginated = Paginator(results, PAGE_LENGTH)
page = paginated.get_page(request.GET.get("page"))
data["results"] = page
data["page_range"] = paginated.get_elided_page_range(
page.number, on_each_side=2, on_ends=1
)
return TemplateResponse(request, "search/list.html", data)
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)
if len(su_num) == 13 and su_num.isdecimal():
# Multiply every other digit by 3
# Add these numbers and the other digits
product = sum(int(ch) for ch in su_num[::2]) + sum(
int(ch) * 3 for ch in su_num[1::2]
)
if product % 10 == 0:
return su_num
elif (
len(su_num) == 10
and su_num[:-1].isdecimal()
and (su_num[-1].isdecimal() or su_num[-1].lower() == "x")
):
product = 0
# Iterate through code_string
for i in range(9):
# for each character, multiply by a different decreasing number: 10 - x
product = product + int(su_num[i]) * (10 - i)
# Handle last character
if su_num[9].lower() == "x":
product += 10
else:
product += int(su_num[9])
if product % 11 == 0:
return su_num
return query