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