def get(self, request): """info about a book""" query = request.GET.get("query") book_results = popular_books = [] if query: book_results = book_search.search(query)[:5] if len(book_results) < 5: popular_books = ( models.Edition.objects.exclude( # exclude already shelved Q(parent_work__in=[ b.book.parent_work for b in request.user.shelfbook_set.distinct().all() ]) | Q( # and exclude if it's already in search results parent_work__in=[b.parent_work for b in book_results])).annotate( Count("shelfbook")). order_by("-shelfbook__count")[:5 - len(book_results)]) data = { "book_results": book_results, "popular_books": popular_books, "next": self.next_view, } return TemplateResponse(request, "get_started/books.html", data)
def test_search(self): """search for a book in the db""" # title/author results = book_search.search("Example") self.assertEqual(len(results), 1) self.assertEqual(results[0], self.first_edition) # isbn results = book_search.search("0000000000") self.assertEqual(len(results), 1) self.assertEqual(results[0], self.first_edition) # identifier results = book_search.search("hello") self.assertEqual(len(results), 1) self.assertEqual(results[0], self.second_edition)
def first_search_result(query, min_confidence=0.1): """search until you find a result that fits""" # try local search first result = book_search.search(query, min_confidence=min_confidence, return_first=True) if result: return result # otherwise, try remote endpoints return search(query, min_confidence=min_confidence, return_first=True) or None
def book_search(query, user, min_confidence, search_remote=False): """the real business is elsewhere""" # try a local-only search results = [{"results": search(query, min_confidence=min_confidence)}] if not user.is_authenticated or (results[0]["results"] and not search_remote): return results, False # if there were no local results, or the request was for remote, search all sources results += connector_manager.search(query, min_confidence=min_confidence) return results, True
def post(self, request): """create a new book""" # returns None if no match is found form = forms.EditionForm(request.POST, request.FILES) data = {"form": form} # collect data provided by the work or import item parent_work_id = request.POST.get("parent_work") authors = None if request.POST.get("authors"): author_ids = findall(r"\d+", request.POST["authors"]) authors = models.Author.objects.filter(id__in=author_ids) # fake book in case we need to keep editing if parent_work_id: data["book"] = { "parent_work": { "id": parent_work_id }, "authors": authors, } if not form.is_valid(): return TemplateResponse(request, "book/edit/edit_book.html", data) data = add_authors(request, data) # check if this is an edition of an existing work author_text = ", ".join(data.get("add_author", [])) data["book_matches"] = book_search.search( f'{form.cleaned_data.get("title")} {author_text}', min_confidence=0.1, )[:5] # go to confirm mode if not parent_work_id or data.get("add_author"): return TemplateResponse(request, "book/edit/edit_book.html", data) with transaction.atomic(): book = form.save() parent_work = get_object_or_404(models.Work, id=parent_work_id) book.parent_work = parent_work if authors: book.authors.add(*authors) url = request.POST.get("cover-url") if url: image = set_cover_from_url(url) if image: book.cover.save(*image, save=False) book.save() return redirect(f"/book/{book.id}")
def get(self, request): """that search bar up top""" query = request.GET.get("q") # check if query is isbn query = isbn_check(query) min_confidence = request.GET.get("min_confidence", 0) search_type = request.GET.get("type") search_remote = (request.GET.get("remote", False) and request.user.is_authenticated) if is_api_request(request): # 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], safe=False) if query and not search_type: search_type = "user" if "@" in query else "book" endpoints = { "book": book_search, "user": user_search, "list": list_search, } if not search_type in endpoints: search_type = "book" data = { "query": query or "", "type": search_type, "remote": search_remote, } if query: results, search_remote = endpoints[search_type](query, request.user, min_confidence, search_remote) if results: paginated = Paginator(results, PAGE_LENGTH).get_page( request.GET.get("page")) data["results"] = paginated data["remote"] = search_remote return TemplateResponse(request, f"search/{search_type}.html", data)
def post(self, request, book_id=None): """edit a book cool""" # returns None if no match is found book = models.Edition.objects.filter(id=book_id).first() form = forms.EditionForm(request.POST, request.FILES, instance=book) data = {"book": book, "form": form} if not form.is_valid(): return TemplateResponse(request, "book/edit/edit_book.html", data) # filter out empty author fields add_author = [ author for author in request.POST.getlist("add_author") if author ] if add_author: data["add_author"] = add_author data["author_matches"] = [] data["isni_matches"] = [] for author in add_author: if not author: continue # check for existing authors vector = SearchVector("name", weight="A") + SearchVector( "aliases", weight="B") author_matches = (models.Author.objects.annotate( search=vector).annotate( rank=SearchRank(vector, author)).filter( rank__gt=0.4).order_by("-rank")[:5]) isni_authors = find_authors_by_name( author, description=True) # find matches from ISNI API # dedupe isni authors we already have in the DB exists = [ i for i in isni_authors for a in author_matches if sub( r"\D", "", str(i.isni)) == sub(r"\D", "", str(a.isni)) ] # pylint: disable=cell-var-from-loop matches = list(filter(lambda x: x not in exists, isni_authors)) # combine existing and isni authors matches.extend(author_matches) data["author_matches"].append({ "name": author.strip(), "matches": matches, "existing_isnis": exists, }) # we're creating a new book if not book: # check if this is an edition of an existing work author_text = book.author_text if book else add_author data["book_matches"] = book_search.search( f'{form.cleaned_data.get("title")} {author_text}', min_confidence=0.5, )[:5] # either of the above cases requires additional confirmation if add_author or not book: # creting a book or adding an author to a book needs another step data["confirm_mode"] = True # this isn't preserved because it isn't part of the form obj data["remove_authors"] = request.POST.getlist("remove_authors") data["cover_url"] = request.POST.get("cover-url") # make sure the dates are passed in as datetime, they're currently a string # QueryDicts are immutable, we need to copy formcopy = data["form"].data.copy() try: formcopy["first_published_date"] = dateparse( formcopy["first_published_date"]) except (MultiValueDictKeyError, ValueError): pass try: formcopy["published_date"] = dateparse( formcopy["published_date"]) except (MultiValueDictKeyError, ValueError): pass data["form"].data = formcopy return TemplateResponse(request, "book/edit/edit_book.html", data) remove_authors = request.POST.getlist("remove_authors") for author_id in remove_authors: book.authors.remove(author_id) book = form.save(commit=False) url = request.POST.get("cover-url") if url: image = set_cover_from_url(url) if image: book.cover.save(*image, save=False) book.save() return redirect(f"/book/{book.id}")
def get(self, request, list_id, add_failed=False, add_succeeded=False): """display a book list""" book_list = get_object_or_404(models.List, id=list_id) book_list.raise_visible_to_user(request.user) if is_api_request(request): return ActivitypubResponse(book_list.to_activity(**request.GET)) query = request.GET.get("q") suggestions = None items = book_list.listitem_set.filter(approved=True).prefetch_related( "user", "book", "book__authors") items = sort_list(request, items) paginated = Paginator(items, PAGE_LENGTH) if query and request.user.is_authenticated: # search for books suggestions = book_search.search( query, filters=[~Q(parent_work__editions__in=book_list.books.all())], ) elif request.user.is_authenticated: # just suggest whatever books are nearby suggestions = request.user.shelfbook_set.filter(~Q( book__in=book_list.books.all())) suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: suggestions += [ s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), ).order_by( "-updated_date") ][:5 - len(suggestions)] page = paginated.get_page(request.GET.get("page")) embed_key = str(book_list.embed_key.hex) embed_url = reverse("embed-list", args=[book_list.id, embed_key]) embed_url = request.build_absolute_uri(embed_url) if request.GET: embed_url = f"{embed_url}?{request.GET.urlencode()}" data = { "list": book_list, "items": page, "page_range": paginated.get_elided_page_range(page.number, on_each_side=2, on_ends=1), "pending_count": book_list.listitem_set.filter(approved=False).count(), "suggested_books": suggestions, "list_form": forms.ListForm(instance=book_list), "query": query or "", "sort_form": forms.SortListForm(request.GET), "embed_url": embed_url, "add_failed": add_failed, "add_succeeded": add_succeeded, } return TemplateResponse(request, "lists/list.html", data)
def get(self, request, list_id): """display a book list""" book_list = get_object_or_404(models.List, id=list_id) book_list.raise_visible_to_user(request.user) if is_api_request(request): return ActivitypubResponse(book_list.to_activity(**request.GET)) query = request.GET.get("q") suggestions = None # sort_by shall be "order" unless a valid alternative is given sort_by = request.GET.get("sort_by", "order") if sort_by not in ("order", "title", "rating"): sort_by = "order" # direction shall be "ascending" unless a valid alternative is given direction = request.GET.get("direction", "ascending") if direction not in ("ascending", "descending"): direction = "ascending" directional_sort_by = { "order": "order", "title": "book__title", "rating": "average_rating", }[sort_by] if direction == "descending": directional_sort_by = "-" + directional_sort_by items = book_list.listitem_set.prefetch_related("user", "book", "book__authors") if sort_by == "rating": items = items.annotate( average_rating=Avg( Coalesce("book__review__rating", 0.0), output_field=DecimalField(), ) ) items = items.filter(approved=True).order_by(directional_sort_by) paginated = Paginator(items, PAGE_LENGTH) if query and request.user.is_authenticated: # search for books suggestions = book_search.search( query, filters=[~Q(parent_work__editions__in=book_list.books.all())], ) elif request.user.is_authenticated: # just suggest whatever books are nearby suggestions = request.user.shelfbook_set.filter( ~Q(book__in=book_list.books.all()) ) suggestions = [s.book for s in suggestions[:5]] if len(suggestions) < 5: suggestions += [ s.default_edition for s in models.Work.objects.filter( ~Q(editions__in=book_list.books.all()), ).order_by("-updated_date") ][: 5 - len(suggestions)] page = paginated.get_page(request.GET.get("page")) embed_key = str(book_list.embed_key.hex) embed_url = reverse("embed-list", args=[book_list.id, embed_key]) embed_url = request.build_absolute_uri(embed_url) if request.GET: embed_url = f"{embed_url}?{request.GET.urlencode()}" data = { "list": book_list, "items": page, "page_range": paginated.get_elided_page_range( page.number, on_each_side=2, on_ends=1 ), "pending_count": book_list.listitem_set.filter(approved=False).count(), "suggested_books": suggestions, "list_form": forms.ListForm(instance=book_list), "query": query or "", "sort_form": forms.SortListForm( {"direction": direction, "sort_by": sort_by} ), "embed_url": embed_url, } return TemplateResponse(request, "lists/list.html", data)