diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book/book.html similarity index 95% rename from bookwyrm/templates/book.html rename to bookwyrm/templates/book/book.html index b9708451d..0d908110b 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book/book.html @@ -41,15 +41,10 @@ {% include 'snippets/shelve_button/shelve_button.html' %} {% if request.user.is_authenticated and not book.cover %} -
-

{% trans "Add cover" %}

-
- {% csrf_token %} - - -
+
+ {% trans "Add cover" as button_text %} + {% include 'snippets/toggle/toggle_button.html' with text=button_text controls_text="add-cover" controls_uid=book.id focus="modal-title-add-cover" class="is-small" %} + {% include 'book/cover_modal.html' with book=book controls_text="add-cover" controls_uid=book.id %}
{% endif %} diff --git a/bookwyrm/templates/book/cover_modal.html b/bookwyrm/templates/book/cover_modal.html new file mode 100644 index 000000000..f09b44951 --- /dev/null +++ b/bookwyrm/templates/book/cover_modal.html @@ -0,0 +1,36 @@ +{% extends 'components/modal.html' %} +{% load i18n %} + +{% block modal-title %} +{% trans "Add cover" %} +{% endblock %} + +{% block modal-form-open %} +
+{% endblock %} + +{% block modal-body %} + +{% endblock %} + +{% block modal-footer %} + +{% trans "Cancel" as button_text %} +{% include 'snippets/toggle/toggle_button.html' with text=button_text %} +{% endblock %} +{% block modal-form-close %}
{% endblock %} + diff --git a/bookwyrm/templates/edit_book.html b/bookwyrm/templates/book/edit_book.html similarity index 93% rename from bookwyrm/templates/edit_book.html rename to bookwyrm/templates/book/edit_book.html index 5c21ac951..d7c842351 100644 --- a/bookwyrm/templates/edit_book.html +++ b/bookwyrm/templates/book/edit_book.html @@ -85,7 +85,7 @@
-
+

{% trans "Metadata" %}

{{ form.title }}

@@ -151,15 +151,26 @@
-
+
+

{% trans "Cover" %}

{% include 'snippets/book_cover.html' with book=book size="small" %}
-

{% trans "Cover" %}

-

{{ form.cover }}

+

+ + {{ form.cover }} +

+ {% if book %} +

+ + +

+ {% endif %} {% for error in form.cover.errors %}

{{ error | escape }}

{% endfor %} diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 850be8db9..ade6131d0 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -1,7 +1,14 @@ """ test for app action functionality """ +from io import BytesIO +import pathlib from unittest.mock import patch + +from PIL import Image +import responses + from django.contrib.auth.models import Group, Permission from django.contrib.contenttypes.models import ContentType +from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -229,3 +236,59 @@ class BookViews(TestCase): 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) + image_file = pathlib.Path(__file__).parent.joinpath( + "../../static/images/default_avi.jpg" + ) + + form = forms.CoverForm(instance=self.book) + form.data["cover"] = SimpleUploadedFile( + image_file, open(image_file, "rb").read(), content_type="image/jpeg" + ) + + request = self.factory.post("", form.data) + request.user = self.local_user + + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: + views.upload_cover(request, self.book.id) + self.assertEqual(delay_mock.call_count, 1) + + self.book.refresh_from_db() + self.assertTrue(self.book.cover) + + @responses.activate + def test_upload_cover_url(self): + """ add a cover via url """ + self.assertFalse(self.book.cover) + image_file = pathlib.Path(__file__).parent.joinpath( + "../../static/images/default_avi.jpg" + ) + image = Image.open(image_file) + output = BytesIO() + image.save(output, format=image.format) + responses.add( + responses.GET, + "http://example.com", + body=output.getvalue(), + status=200, + ) + + form = forms.CoverForm(instance=self.book) + form.data["cover-url"] = "http://example.com" + + request = self.factory.post("", form.data) + request.user = self.local_user + + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: + views.upload_cover(request, self.book.id) + self.assertEqual(delay_mock.call_count, 1) + + self.book.refresh_from_db() + self.assertTrue(self.book.cover) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 426c1a791..1708ec530 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -5,6 +5,7 @@ from PIL import Image from django.contrib.auth.models import AnonymousUser from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -157,28 +158,31 @@ class UserViews(TestCase): self.assertEqual(self.local_user.email, "wow@email.com") # idk how to mock the upload form, got tired of triyng to make it work - # def test_edit_user_avatar(self): - # ''' use a form to update a user ''' - # view = views.EditUser.as_view() - # form = forms.EditUserForm(instance=self.local_user) - # form.data['name'] = 'New Name' - # form.data['email'] = 'wow@email.com' - # image_file = pathlib.Path(__file__).parent.joinpath( - # '../../static/images/no_cover.jpg') - # image = Image.open(image_file) - # form.files['avatar'] = SimpleUploadedFile( - # image_file, open(image_file), content_type='image/jpeg') - # request = self.factory.post('', form.data, form.files) - # request.user = self.local_user + def test_edit_user_avatar(self): + """ use a form to update a user """ + view = views.EditUser.as_view() + form = forms.EditUserForm(instance=self.local_user) + form.data["name"] = "New Name" + form.data["email"] = "wow@email.com" + image_file = pathlib.Path(__file__).parent.joinpath( + "../../static/images/no_cover.jpg" + ) + form.data["avatar"] = SimpleUploadedFile( + image_file, open(image_file, "rb").read(), content_type="image/jpeg" + ) + request = self.factory.post("", form.data) + request.user = self.local_user - # with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ - # as delay_mock: - # view(request) - # self.assertEqual(delay_mock.call_count, 1) - # self.assertEqual(self.local_user.name, 'New Name') - # self.assertEqual(self.local_user.email, 'wow@email.com') - # self.assertIsNotNone(self.local_user.avatar) - # self.assertEqual(self.local_user.avatar.size, (120, 120)) + with patch( + "bookwyrm.models.activitypub_mixin.broadcast_task.delay" + ) as delay_mock: + view(request) + self.assertEqual(delay_mock.call_count, 1) + self.assertEqual(self.local_user.name, "New Name") + self.assertEqual(self.local_user.email, "wow@email.com") + self.assertIsNotNone(self.local_user.avatar) + self.assertEqual(self.local_user.avatar.width, 120) + self.assertEqual(self.local_user.avatar.height, 120) def test_crop_avatar(self): """ reduce that image size """ diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 688c8a777..44492668b 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -152,7 +152,9 @@ urlpatterns = [ re_path(r"^create-book/?$", views.EditBook.as_view()), re_path(r"^create-book/confirm?$", views.ConfirmEditBook.as_view()), re_path(r"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()), - re_path(r"^upload-cover/(?P\d+)/?$", views.upload_cover), + re_path( + r"^upload-cover/(?P\d+)/?$", views.upload_cover, name="upload-cover" + ), re_path(r"^add-description/(?P\d+)/?$", views.add_description), re_path(r"^resolve-book/?$", views.resolve_book), re_path(r"^switch-edition/?$", views.switch_edition), diff --git a/bookwyrm/views/books.py b/bookwyrm/views/books.py index 0b23b0d63..f2aa76d79 100644 --- a/bookwyrm/views/books.py +++ b/bookwyrm/views/books.py @@ -1,6 +1,9 @@ """ the good stuff! the books! """ +from uuid import uuid4 + from django.contrib.auth.decorators import login_required, permission_required from django.contrib.postgres.search import SearchRank, SearchVector +from django.core.files.base import ContentFile from django.core.paginator import Paginator from django.db import transaction from django.db.models import Avg, Q @@ -14,6 +17,7 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.connectors import connector_manager +from bookwyrm.connectors.abstract_connector import get_image from bookwyrm.settings import PAGE_LENGTH from .helpers import is_api_request, get_activity_feed, get_edition from .helpers import privacy_filter @@ -97,7 +101,7 @@ class Book(View): "readthroughs": readthroughs, "path": "/book/%s" % book_id, } - return TemplateResponse(request, "book.html", data) + return TemplateResponse(request, "book/book.html", data) @method_decorator(login_required, name="dispatch") @@ -115,7 +119,7 @@ class EditBook(View): if not book.description: book.description = book.parent_work.description data = {"book": book, "form": forms.EditionForm(instance=book)} - return TemplateResponse(request, "edit_book.html", data) + return TemplateResponse(request, "book/edit_book.html", data) def post(self, request, book_id=None): """ edit a book cool """ @@ -125,7 +129,7 @@ class EditBook(View): data = {"book": book, "form": form} if not form.is_valid(): - return TemplateResponse(request, "edit_book.html", data) + return TemplateResponse(request, "book/edit_book.html", data) add_author = request.POST.get("add_author") # we're adding an author through a free text field @@ -169,13 +173,19 @@ class EditBook(View): 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") - return TemplateResponse(request, "edit_book.html", data) + return TemplateResponse(request, "book/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() + 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("/book/%s" % book.id) @@ -194,7 +204,7 @@ class ConfirmEditBook(View): data = {"book": book, "form": form} if not form.is_valid(): - return TemplateResponse(request, "edit_book.html", data) + return TemplateResponse(request, "book/edit_book.html", data) with transaction.atomic(): # save book @@ -256,18 +266,35 @@ class Editions(View): def upload_cover(request, book_id): """ upload a new cover """ book = get_object_or_404(models.Edition, id=book_id) + book.last_edited_by = request.user + + url = request.POST.get("cover-url") + if url: + image = set_cover_from_url(url) + book.cover.save(*image) - form = forms.CoverForm(request.POST, request.FILES, instance=book) - if not form.is_valid(): return redirect("/book/%d" % book.id) - book.last_edited_by = request.user + form = forms.CoverForm(request.POST, request.FILES, instance=book) + if not form.is_valid() or not form.files.get("cover"): + return redirect("/book/%d" % book.id) + book.cover = form.files["cover"] book.save() return redirect("/book/%s" % book.id) +def set_cover_from_url(url): + """ load it from a url """ + image_file = get_image(url) + if not image_file: + return None + image_name = str(uuid4()) + "." + url.split(".")[-1] + image_content = ContentFile(image_file.content) + return [image_name, image_content] + + @login_required @require_POST @permission_required("bookwyrm.edit_book", raise_exception=True) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 469a82d38..690bf158c 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -173,7 +173,7 @@ class EditUser(View): # set the name to a hash extension = form.files["avatar"].name.split(".")[-1] filename = "%s.%s" % (uuid4(), extension) - user.avatar.save(filename, image) + user.avatar.save(filename, image, save=False) user.save() return redirect(user.local_path)