diff --git a/bookwyrm/templates/book/edition_filters.html b/bookwyrm/templates/book/edition_filters.html index a55b72af..c41ab0c0 100644 --- a/bookwyrm/templates/book/edition_filters.html +++ b/bookwyrm/templates/book/edition_filters.html @@ -1,6 +1,7 @@ {% extends 'snippets/filters_panel/filters_panel.html' %} {% block filter_fields %} +{% include 'book/search_filter.html' %} {% include 'book/language_filter.html' %} {% include 'book/format_filter.html' %} {% endblock %} diff --git a/bookwyrm/templates/book/search_filter.html b/bookwyrm/templates/book/search_filter.html new file mode 100644 index 00000000..f2345a68 --- /dev/null +++ b/bookwyrm/templates/book/search_filter.html @@ -0,0 +1,8 @@ +{% extends 'snippets/filters_panel/filter_field.html' %} +{% load i18n %} + +{% block filter %} + + +{% endblock %} + diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index c5d86a12..2cd50302 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -280,49 +280,6 @@ class BookViews(TestCase): self.assertEqual(book.authors.first().name, "Sappho") self.assertEqual(book.authors.first(), book.parent_work.authors.first()) - @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") - def test_switch_edition(self, _): - """updates user's relationships to a book""" - work = models.Work.objects.create(title="test work") - edition1 = models.Edition.objects.create(title="first ed", parent_work=work) - edition2 = models.Edition.objects.create(title="second ed", parent_work=work) - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) - models.ShelfBook.objects.create( - book=edition1, - user=self.local_user, - shelf=shelf, - ) - models.ReadThrough.objects.create(user=self.local_user, book=edition1) - - self.assertEqual(models.ShelfBook.objects.get().book, edition1) - self.assertEqual(models.ReadThrough.objects.get().book, edition1) - request = self.factory.post("", {"edition": edition2.id}) - request.user = self.local_user - with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): - views.switch_edition(request) - - self.assertEqual(models.ShelfBook.objects.get().book, edition2) - self.assertEqual(models.ReadThrough.objects.get().book, edition2) - - def test_editions_page(self): - """there are so many views, this just makes sure it LOADS""" - view = views.Editions.as_view() - request = self.factory.get("") - with patch("bookwyrm.views.books.is_api_request") as is_api: - is_api.return_value = False - result = view(request, self.work.id) - self.assertIsInstance(result, TemplateResponse) - result.render() - self.assertEqual(result.status_code, 200) - - request = self.factory.get("") - with patch("bookwyrm.views.books.is_api_request") as is_api: - is_api.return_value = True - result = view(request, self.work.id) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - def test_upload_cover_file(self): """add a cover via file upload""" self.assertFalse(self.book.cover) diff --git a/bookwyrm/tests/views/test_editions.py b/bookwyrm/tests/views/test_editions.py new file mode 100644 index 00000000..1bd23ae1 --- /dev/null +++ b/bookwyrm/tests/views/test_editions.py @@ -0,0 +1,126 @@ +""" test for app action functionality """ +from unittest.mock import patch + +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views +from bookwyrm.activitypub import ActivitypubResponse + + +class BookViews(TestCase): + """books books books""" + + def setUp(self): + """we need basic test data and mocks""" + self.factory = RequestFactory() + with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"): + self.local_user = models.User.objects.create_user( + "mouse@local.com", + "mouse@mouse.com", + "mouseword", + local=True, + localname="mouse", + remote_id="https://example.com/users/mouse", + ) + self.work = models.Work.objects.create(title="Test Work") + self.book = models.Edition.objects.create( + title="Example Edition", + remote_id="https://example.com/book/1", + parent_work=self.work, + physical_format="paperback", + ) + + models.SiteSettings.objects.create() + + def test_editions_page(self): + """there are so many views, this just makes sure it LOADS""" + view = views.Editions.as_view() + request = self.factory.get("") + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.work.id) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + self.assertTrue("paperback" in result.context_data["formats"]) + + def test_editions_page_filtered(self): + """editions view with filters""" + models.Edition.objects.create( + title="Fish", + physical_format="okay", + parent_work=self.work, + ) + view = views.Editions.as_view() + request = self.factory.get("") + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.work.id) + self.assertIsInstance(result, TemplateResponse) + result.render() + self.assertEqual(result.status_code, 200) + self.assertEqual(len(result.context_data["editions"].object_list), 2) + self.assertEqual(len(result.context_data["formats"]), 2) + self.assertTrue("paperback" in result.context_data["formats"]) + self.assertTrue("okay" in result.context_data["formats"]) + + request = self.factory.get("", {"q": "fish"}) + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.work.id) + result.render() + self.assertEqual(result.status_code, 200) + self.assertEqual(len(result.context_data["editions"].object_list), 1) + + request = self.factory.get("", {"q": "okay"}) + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.work.id) + result.render() + self.assertEqual(result.status_code, 200) + self.assertEqual(len(result.context_data["editions"].object_list), 1) + + request = self.factory.get("", {"format": "okay"}) + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = False + result = view(request, self.work.id) + result.render() + self.assertEqual(result.status_code, 200) + self.assertEqual(len(result.context_data["editions"].object_list), 1) + + def test_editions_page_api(self): + """there are so many views, this just makes sure it LOADS""" + view = views.Editions.as_view() + request = self.factory.get("") + with patch("bookwyrm.views.editions.is_api_request") as is_api: + is_api.return_value = True + result = view(request, self.work.id) + self.assertIsInstance(result, ActivitypubResponse) + self.assertEqual(result.status_code, 200) + + @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") + def test_switch_edition(self, _): + """updates user's relationships to a book""" + work = models.Work.objects.create(title="test work") + edition1 = models.Edition.objects.create(title="first ed", parent_work=work) + edition2 = models.Edition.objects.create(title="second ed", parent_work=work) + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + shelf = models.Shelf.objects.create(name="Test Shelf", user=self.local_user) + models.ShelfBook.objects.create( + book=edition1, + user=self.local_user, + shelf=shelf, + ) + models.ReadThrough.objects.create(user=self.local_user, book=edition1) + + self.assertEqual(models.ShelfBook.objects.get().book, edition1) + self.assertEqual(models.ReadThrough.objects.get().book, edition1) + request = self.factory.post("", {"edition": edition2.id}) + request.user = self.local_user + with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"): + views.switch_edition(request) + + self.assertEqual(models.ShelfBook.objects.get().book, edition2) + self.assertEqual(models.ReadThrough.objects.get().book, edition2) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 15b55acc..303dad62 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -4,11 +4,12 @@ from .authentication import Login, Register, Logout from .authentication import ConfirmEmail, ConfirmEmailCode, resend_link from .author import Author, EditAuthor from .block import Block, unblock -from .books import Book, EditBook, ConfirmEditBook, Editions -from .books import upload_cover, add_description, switch_edition, resolve_book +from .books import Book, EditBook, ConfirmEditBook +from .books import upload_cover, add_description, resolve_book from .directory import Directory from .discover import Discover from .edit_user import EditUser, DeleteUser +from .editions import Editions, switch_edition from .federation import Federation, FederatedServer from .federation import AddFederatedServer, ImportServerBlocklist from .federation import block_server, unblock_server diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 6cd0427c..97c23def 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -24,7 +24,7 @@ from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_edition, privacy_filter -# pylint: disable= no-self-use +# pylint: disable=no-self-use class Book(View): """a book! this is the stuff""" @@ -270,37 +270,6 @@ class ConfirmEditBook(View): return redirect("/book/%s" % book.id) -class Editions(View): - """list of editions""" - - def get(self, request, book_id): - """list of editions of a book""" - work = get_object_or_404(models.Work, id=book_id) - - if is_api_request(request): - return ActivitypubResponse(work.to_edition_list(**request.GET)) - filters = {} - - if request.GET.get("language"): - filters["languages__contains"] = [request.GET.get("language")] - if request.GET.get("format"): - filters["physical_format__iexact"] = request.GET.get("format") - - editions = work.editions.order_by("-edition_rank") - languages = set(sum([e.languages for e in editions], [])) - - paginated = Paginator(editions.filter(**filters), PAGE_LENGTH) - data = { - "editions": paginated.get_page(request.GET.get("page")), - "work": work, - "languages": languages, - "formats": set( - e.physical_format.lower() for e in editions if e.physical_format - ), - } - return TemplateResponse(request, "book/editions.html", data) - - @login_required @require_POST def upload_cover(request, book_id): @@ -363,33 +332,3 @@ def resolve_book(request): book = connector.get_or_create_book(remote_id) return redirect("book", book.id) - - -@login_required -@require_POST -@transaction.atomic -def switch_edition(request): - """switch your copy of a book to a different edition""" - edition_id = request.POST.get("edition") - new_edition = get_object_or_404(models.Edition, id=edition_id) - shelfbooks = models.ShelfBook.objects.filter( - book__parent_work=new_edition.parent_work, shelf__user=request.user - ) - for shelfbook in shelfbooks.all(): - with transaction.atomic(): - models.ShelfBook.objects.create( - created_date=shelfbook.created_date, - user=shelfbook.user, - shelf=shelfbook.shelf, - book=new_edition, - ) - shelfbook.delete() - - readthroughs = models.ReadThrough.objects.filter( - book__parent_work=new_edition.parent_work, user=request.user - ) - for readthrough in readthroughs.all(): - readthrough.book = new_edition - readthrough.save() - - return redirect("/book/%d" % new_edition.id) diff --git a/bookwyrm/views/editions.py b/bookwyrm/views/editions.py new file mode 100644 index 00000000..7615c497 --- /dev/null +++ b/bookwyrm/views/editions.py @@ -0,0 +1,99 @@ +""" the good stuff! the books! """ +from functools import reduce +import operator + +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.db import transaction +from django.db.models import Q +from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse +from django.views import View +from django.views.decorators.http import require_POST + +from bookwyrm import models +from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.settings import PAGE_LENGTH +from .helpers import is_api_request + + +# pylint: disable=no-self-use +class Editions(View): + """list of editions""" + + def get(self, request, book_id): + """list of editions of a book""" + work = get_object_or_404(models.Work, id=book_id) + + if is_api_request(request): + return ActivitypubResponse(work.to_edition_list(**request.GET)) + filters = {} + + if request.GET.get("language"): + filters["languages__contains"] = [request.GET.get("language")] + if request.GET.get("format"): + filters["physical_format__iexact"] = request.GET.get("format") + + editions = work.editions.order_by("-edition_rank") + languages = set(sum(editions.values_list("languages", flat=True), [])) + + editions = editions.filter(**filters) + + query = request.GET.get("q") + if query: + searchable_array_fields = ["languages", "publishers"] + searchable_fields = [ + "title", + "physical_format", + "isbn_10", + "isbn_13", + "oclc_number", + "asin", + ] + search_filter_entries = [ + {f"{f}__icontains": query} for f in searchable_fields + ] + [{f"{f}__iexact": query} for f in searchable_array_fields] + editions = editions.filter( + reduce(operator.or_, (Q(**f) for f in search_filter_entries)) + ) + + paginated = Paginator(editions, PAGE_LENGTH) + data = { + "editions": paginated.get_page(request.GET.get("page")), + "work": work, + "languages": languages, + "formats": set( + e.physical_format.lower() for e in editions if e.physical_format + ), + } + return TemplateResponse(request, "book/editions.html", data) + + +@login_required +@require_POST +@transaction.atomic +def switch_edition(request): + """switch your copy of a book to a different edition""" + edition_id = request.POST.get("edition") + new_edition = get_object_or_404(models.Edition, id=edition_id) + shelfbooks = models.ShelfBook.objects.filter( + book__parent_work=new_edition.parent_work, shelf__user=request.user + ) + for shelfbook in shelfbooks.all(): + with transaction.atomic(): + models.ShelfBook.objects.create( + created_date=shelfbook.created_date, + user=shelfbook.user, + shelf=shelfbook.shelf, + book=new_edition, + ) + shelfbook.delete() + + readthroughs = models.ReadThrough.objects.filter( + book__parent_work=new_edition.parent_work, user=request.user + ) + for readthrough in readthroughs.all(): + readthrough.book = new_edition + readthrough.save() + + return redirect("/book/%d" % new_edition.id)