Merge pull request #761 from mouse-reeve/cover-url

Cover url
This commit is contained in:
Mouse Reeve 2021-03-19 10:54:53 -07:00 committed by GitHub
commit 17a3a06891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 183 additions and 45 deletions

View file

@ -41,15 +41,10 @@
{% include 'snippets/shelve_button/shelve_button.html' %} {% include 'snippets/shelve_button/shelve_button.html' %}
{% if request.user.is_authenticated and not book.cover %} {% if request.user.is_authenticated and not book.cover %}
<div class="box p-2"> <div class="block">
<h3 class="title is-6 mb-1">{% trans "Add cover" %}</h3> {% trans "Add cover" as button_text %}
<form name="add-cover" method="POST" action="/upload-cover/{{ book.id }}" enctype="multipart/form-data"> {% 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" %}
{% csrf_token %} {% include 'book/cover_modal.html' with book=book controls_text="add-cover" controls_uid=book.id %}
<label class="label">
<input type="file" name="cover" accept="image/*" enctype="multipart/form-data" id="id_cover" required>
</label>
<button class="button is-small is-primary" type="submit">{% trans "Add" %}</button>
</form>
</div> </div>
{% endif %} {% endif %}

View file

@ -0,0 +1,36 @@
{% extends 'components/modal.html' %}
{% load i18n %}
{% block modal-title %}
{% trans "Add cover" %}
{% endblock %}
{% block modal-form-open %}
<form name="add-cover" method="POST" action="{% url 'upload-cover' book.id %}" enctype="multipart/form-data">
{% endblock %}
{% block modal-body %}
<section class="modal-card-body columns">
{% csrf_token %}
<div class="column">
<label class="label" for="id_cover">
{% trans "Upload cover:" %}
</label>
<input type="file" name="cover" accept="image/*" enctype="multipart/form-data" id="id_cover">
</div>
<div class="column">
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
</label>
<input class="input" name="cover-url" id="id_cover_url">
</div>
</section>
{% endblock %}
{% block modal-footer %}
<button class="button is-primary" type="submit">{% trans "Add" %}</button>
{% trans "Cancel" as button_text %}
{% include 'snippets/toggle/toggle_button.html' with text=button_text %}
{% endblock %}
{% block modal-form-close %}</form>{% endblock %}

View file

@ -85,7 +85,7 @@
<input type="hidden" name="last_edited_by" value="{{ request.user.id }}"> <input type="hidden" name="last_edited_by" value="{{ request.user.id }}">
<div class="columns"> <div class="columns">
<div class="column"> <div class="column is-half">
<section class="block"> <section class="block">
<h2 class="title is-4">{% trans "Metadata" %}</h2> <h2 class="title is-4">{% trans "Metadata" %}</h2>
<p class="mb-2"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p> <p class="mb-2"><label class="label" for="id_title">{% trans "Title:" %}</label> {{ form.title }} </p>
@ -151,15 +151,26 @@
</section> </section>
</div> </div>
<div class="column"> <div class="column is-half">
<h2 class="title is-4">{% trans "Cover" %}</h2>
<div class="columns"> <div class="columns">
<div class="column is-narrow"> <div class="column is-narrow">
{% include 'snippets/book_cover.html' with book=book size="small" %} {% include 'snippets/book_cover.html' with book=book size="small" %}
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<div class="block"> <div class="block">
<h2 class="title is-4">{% trans "Cover" %}</h2> <p>
<p>{{ form.cover }}</p> <label class="label" for="id_cover">{% trans "Upload cover:" %}</label>
{{ form.cover }}
</p>
{% if book %}
<p>
<label class="label" for="id_cover_url">
{% trans "Load cover from url:" %}
</label>
<input class="input" name="cover-url" id="id_cover_url">
</p>
{% endif %}
{% for error in form.cover.errors %} {% for error in form.cover.errors %}
<p class="help is-danger">{{ error | escape }}</p> <p class="help is-danger">{{ error | escape }}</p>
{% endfor %} {% endfor %}

View file

@ -1,7 +1,14 @@
""" test for app action functionality """ """ test for app action functionality """
from io import BytesIO
import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image
import responses
from django.contrib.auth.models import Group, Permission from django.contrib.auth.models import Group, Permission
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -229,3 +236,59 @@ class BookViews(TestCase):
result = view(request, self.work.id) result = view(request, self.work.id)
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) 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)

View file

@ -5,6 +5,7 @@ from PIL import Image
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
from django.core.files.uploadedfile import SimpleUploadedFile
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
@ -157,28 +158,31 @@ class UserViews(TestCase):
self.assertEqual(self.local_user.email, "wow@email.com") self.assertEqual(self.local_user.email, "wow@email.com")
# idk how to mock the upload form, got tired of triyng to make it work # idk how to mock the upload form, got tired of triyng to make it work
# def test_edit_user_avatar(self): def test_edit_user_avatar(self):
# ''' use a form to update a user ''' """ use a form to update a user """
# view = views.EditUser.as_view() view = views.EditUser.as_view()
# form = forms.EditUserForm(instance=self.local_user) form = forms.EditUserForm(instance=self.local_user)
# form.data['name'] = 'New Name' form.data["name"] = "New Name"
# form.data['email'] = 'wow@email.com' form.data["email"] = "wow@email.com"
# image_file = pathlib.Path(__file__).parent.joinpath( image_file = pathlib.Path(__file__).parent.joinpath(
# '../../static/images/no_cover.jpg') "../../static/images/no_cover.jpg"
# image = Image.open(image_file) )
# form.files['avatar'] = SimpleUploadedFile( form.data["avatar"] = SimpleUploadedFile(
# image_file, open(image_file), content_type='image/jpeg') image_file, open(image_file, "rb").read(), content_type="image/jpeg"
# request = self.factory.post('', form.data, form.files) )
# request.user = self.local_user request = self.factory.post("", form.data)
request.user = self.local_user
# with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \ with patch(
# as delay_mock: "bookwyrm.models.activitypub_mixin.broadcast_task.delay"
# view(request) ) as delay_mock:
# self.assertEqual(delay_mock.call_count, 1) view(request)
# self.assertEqual(self.local_user.name, 'New Name') self.assertEqual(delay_mock.call_count, 1)
# self.assertEqual(self.local_user.email, 'wow@email.com') self.assertEqual(self.local_user.name, "New Name")
# self.assertIsNotNone(self.local_user.avatar) self.assertEqual(self.local_user.email, "wow@email.com")
# self.assertEqual(self.local_user.avatar.size, (120, 120)) 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): def test_crop_avatar(self):
""" reduce that image size """ """ reduce that image size """

View file

@ -152,7 +152,9 @@ urlpatterns = [
re_path(r"^create-book/?$", views.EditBook.as_view()), re_path(r"^create-book/?$", views.EditBook.as_view()),
re_path(r"^create-book/confirm?$", views.ConfirmEditBook.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"%s/editions(.json)?/?$" % book_path, views.Editions.as_view()),
re_path(r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover), re_path(
r"^upload-cover/(?P<book_id>\d+)/?$", views.upload_cover, name="upload-cover"
),
re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description), re_path(r"^add-description/(?P<book_id>\d+)/?$", views.add_description),
re_path(r"^resolve-book/?$", views.resolve_book), re_path(r"^resolve-book/?$", views.resolve_book),
re_path(r"^switch-edition/?$", views.switch_edition), re_path(r"^switch-edition/?$", views.switch_edition),

View file

@ -1,6 +1,9 @@
""" the good stuff! the books! """ """ the good stuff! the books! """
from uuid import uuid4
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.postgres.search import SearchRank, SearchVector from django.contrib.postgres.search import SearchRank, SearchVector
from django.core.files.base import ContentFile
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db import transaction from django.db import transaction
from django.db.models import Avg, Q 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 import forms, models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
from bookwyrm.connectors import connector_manager from bookwyrm.connectors import connector_manager
from bookwyrm.connectors.abstract_connector import get_image
from bookwyrm.settings import PAGE_LENGTH from bookwyrm.settings import PAGE_LENGTH
from .helpers import is_api_request, get_activity_feed, get_edition from .helpers import is_api_request, get_activity_feed, get_edition
from .helpers import privacy_filter from .helpers import privacy_filter
@ -97,7 +101,7 @@ class Book(View):
"readthroughs": readthroughs, "readthroughs": readthroughs,
"path": "/book/%s" % book_id, "path": "/book/%s" % book_id,
} }
return TemplateResponse(request, "book.html", data) return TemplateResponse(request, "book/book.html", data)
@method_decorator(login_required, name="dispatch") @method_decorator(login_required, name="dispatch")
@ -115,7 +119,7 @@ class EditBook(View):
if not book.description: if not book.description:
book.description = book.parent_work.description book.description = book.parent_work.description
data = {"book": book, "form": forms.EditionForm(instance=book)} 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): def post(self, request, book_id=None):
""" edit a book cool """ """ edit a book cool """
@ -125,7 +129,7 @@ class EditBook(View):
data = {"book": book, "form": form} data = {"book": book, "form": form}
if not form.is_valid(): 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") add_author = request.POST.get("add_author")
# we're adding an author through a free text field # we're adding an author through a free text field
@ -169,13 +173,19 @@ class EditBook(View):
data["confirm_mode"] = True data["confirm_mode"] = True
# this isn't preserved because it isn't part of the form obj # this isn't preserved because it isn't part of the form obj
data["remove_authors"] = request.POST.getlist("remove_authors") 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") remove_authors = request.POST.getlist("remove_authors")
for author_id in remove_authors: for author_id in remove_authors:
book.authors.remove(author_id) 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) return redirect("/book/%s" % book.id)
@ -194,7 +204,7 @@ class ConfirmEditBook(View):
data = {"book": book, "form": form} data = {"book": book, "form": form}
if not form.is_valid(): if not form.is_valid():
return TemplateResponse(request, "edit_book.html", data) return TemplateResponse(request, "book/edit_book.html", data)
with transaction.atomic(): with transaction.atomic():
# save book # save book
@ -256,18 +266,35 @@ class Editions(View):
def upload_cover(request, book_id): def upload_cover(request, book_id):
""" upload a new cover """ """ upload a new cover """
book = get_object_or_404(models.Edition, id=book_id) 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) 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.cover = form.files["cover"]
book.save() book.save()
return redirect("/book/%s" % book.id) 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 @login_required
@require_POST @require_POST
@permission_required("bookwyrm.edit_book", raise_exception=True) @permission_required("bookwyrm.edit_book", raise_exception=True)

View file

@ -173,7 +173,7 @@ class EditUser(View):
# set the name to a hash # set the name to a hash
extension = form.files["avatar"].name.split(".")[-1] extension = form.files["avatar"].name.split(".")[-1]
filename = "%s.%s" % (uuid4(), extension) filename = "%s.%s" % (uuid4(), extension)
user.avatar.save(filename, image) user.avatar.save(filename, image, save=False)
user.save() user.save()
return redirect(user.local_path) return redirect(user.local_path)