bookwyrm/bookwyrm/views/search.py

219 lines
6.9 KiB
Python
Raw Normal View History

2021-03-08 16:49:10 +00:00
""" search views"""
2024-02-03 20:55:46 +00:00
2021-01-13 20:03:27 +00:00
import re
from django.contrib.postgres.search import TrigramSimilarity
2021-05-01 17:47:01 +00:00
from django.core.paginator import Paginator
2021-01-13 20:03:27 +00:00
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
2021-01-13 20:03:27 +00:00
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
2021-01-13 20:03:27 +00:00
from bookwyrm.utils import regex
2021-10-06 17:37:09 +00:00
from .helpers import is_api_request
2021-01-13 20:03:27 +00:00
from .helpers import handle_remote_webfinger
# pylint: disable= no-self-use
class Search(View):
2021-04-26 16:15:42 +00:00
"""search users or books"""
2021-03-08 16:49:10 +00:00
2023-02-03 20:03:52 +00:00
@csp_update(IMG_SRC="*")
2021-05-01 01:59:02 +00:00
def get(self, request):
2021-04-26 16:15:42 +00:00
"""that search bar up top"""
2021-01-13 20:03:27 +00:00
if is_api_request(request):
return api_book_search(request)
2021-01-13 20:03:27 +00:00
query = request.GET.get("q")
2022-08-05 15:56:24 +00:00
if not query:
return TemplateResponse(request, "search/book.html")
search_type = request.GET.get("type")
2021-05-20 23:34:32 +00:00
if query and not search_type:
2021-05-01 01:59:02 +00:00
search_type = "user" if "@" in query else "book"
endpoints = {
"book": book_search,
2024-02-03 20:55:46 +00:00
"author": author_search,
2021-05-01 01:59:02 +00:00
"user": user_search,
"list": list_search,
}
if not search_type in endpoints:
search_type = "book"
return endpoints[search_type](request)
2021-05-01 01:35:09 +00:00
2021-05-01 01:06:30 +00:00
def api_book_search(request):
"""Return books via API response"""
query = request.GET.get("q")
2023-11-27 23:03:59 +00:00
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)
2022-08-05 18:43:11 +00:00
return JsonResponse(
[format_search_result(r) for r in book_results[:10]], safe=False
)
2022-08-04 19:19:26 +00:00
2021-05-01 01:06:30 +00:00
def book_search(request):
2021-05-01 02:19:10 +00:00
"""the real business is elsewhere"""
query = request.GET.get("q")
# check if query is isbn
2023-11-27 23:03:59 +00:00
query = isbn_check_and_format(query)
min_confidence = request.GET.get("min_confidence", 0)
2022-08-04 19:19:26 +00:00
search_remote = request.GET.get("remote", False) and request.user.is_authenticated
2021-09-14 22:26:18 +00:00
# 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
2022-08-04 19:19:26 +00:00
),
}
# 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)
2021-05-01 01:06:30 +00:00
2024-02-03 20:55:46 +00:00
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}
2021-05-01 01:06:30 +00:00
# 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:
2021-05-01 01:06:30 +00:00
handle_remote_webfinger(query)
results = (
2021-05-01 02:19:10 +00:00
models.User.viewer_aware_objects(viewer)
.annotate(
similarity=Greatest(
TrigramSimilarity("username", query),
TrigramSimilarity("localname", query),
2021-03-08 16:49:10 +00:00
)
2021-05-01 02:19:10 +00:00
)
.filter(
similarity__gt=0.5,
)
.exclude(localname=INSTANCE_ACTOR_USERNAME)
2021-10-03 16:38:41 +00:00
.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)
2021-05-01 01:06:30 +00:00
2021-01-13 20:03:27 +00:00
def list_search(request):
2021-05-01 01:06:30 +00:00
"""any relevent lists?"""
query = request.GET.get("q")
data = {"query": query, "type": "list"}
results = (
2021-10-06 17:37:09 +00:00
models.List.privacy_filter(
request.user,
2021-05-01 02:19:10 +00:00
privacy_levels=["public", "followers"],
)
.annotate(
similarity=Greatest(
TrigramSimilarity("name", query),
TrigramSimilarity("description", query),
2021-02-01 19:50:47 +00:00
)
2021-05-01 02:19:10 +00:00
)
.filter(
similarity__gt=0.1,
)
2021-10-03 16:38:41 +00:00
.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)
2022-02-13 18:07:25 +00:00
2023-11-27 23:03:59 +00:00
def isbn_check_and_format(query):
2022-02-13 19:49:44 +00:00
"""isbn10 or isbn13 check, if so remove separators"""
2022-02-13 19:30:11 +00:00
if query:
su_num = re.sub(r"(?<=\d)\D(?=\d|[xX])", "", query)
if len(su_num) == 13 and su_num.isdecimal():
2022-02-13 19:30:11 +00:00
# Multiply every other digit by 3
# Add these numbers and the other digits
2022-02-13 19:49:44 +00:00
product = sum(int(ch) for ch in su_num[::2]) + sum(
int(ch) * 3 for ch in su_num[1::2]
)
2022-02-13 19:30:11 +00:00
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
2022-02-13 18:07:25 +00:00
return query