Merge branch 'main' into remote-follow

This commit is contained in:
Mouse Reeve
2021-12-02 12:16:56 -08:00
committed by GitHub
24 changed files with 409 additions and 114 deletions

View File

@ -296,7 +296,7 @@ class ManyToManyField(ActivitypubFieldMixin, models.ManyToManyField):
super().__init__(*args, **kwargs)
def set_field_from_activity(self, instance, data, overwrite=True):
"""helper function for assinging a value to the field"""
"""helper function for assigning a value to the field"""
if not overwrite and getattr(instance, self.name).exists():
return False
@ -398,7 +398,11 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
if formatted is None or formatted is MISSING:
return False
if not overwrite and hasattr(instance, self.name):
if (
not overwrite
and hasattr(instance, self.name)
and getattr(instance, self.name)
):
return False
getattr(instance, self.name).save(*formatted, save=save)

View File

@ -45,6 +45,13 @@ let BookWyrm = new class {
'change',
this.disableIfTooLarge.bind(this)
));
document.querySelectorAll('[data-duplicate]')
.forEach(node => node.addEventListener(
'click',
this.duplicateInput.bind(this)
))
}
/**
@ -383,4 +390,24 @@ let BookWyrm = new class {
"left=100,top=100,width=430,height=600"
);
}
duplicateInput (event ) {
const trigger = event.currentTarget;
const input_id = trigger.dataset['duplicate']
const orig = document.getElementById(input_id);
const parent = orig.parentNode;
const new_count = parent.querySelectorAll("input").length + 1
let input = orig.cloneNode();
input.id += ("-" + (new_count))
input.value = ""
let label = parent.querySelector("label").cloneNode();
label.setAttribute("for", input.id)
parent.appendChild(label)
parent.appendChild(input)
}
}();

View File

@ -187,6 +187,7 @@ let StatusCache = new class {
.forEach(item => BookWyrm.addRemoveClass(item, "is-hidden", false));
// Remove existing disabled states
button.querySelectorAll("[data-shelf-dropdown-identifier] button")
.forEach(item => item.disabled = false);

View File

@ -153,12 +153,21 @@
{# user's relationship to the book #}
<div class="block">
{% if user_shelfbooks.count > 0 %}
<h2 class="title is-5">
{% trans "You have shelved this edition in:" %}
</h2>
<ul>
{% for shelf in user_shelfbooks %}
<p>
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}This edition is on your <a href="{{ path }}">{{ shelf_name }}</a> shelf.{% endblocktrans %}
{% include 'snippets/shelf_selector.html' with current=shelf.shelf %}
</p>
<li class="box">
{% blocktrans with path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}<a href="{{ path }}">{{ shelf_name }}</a>{% endblocktrans %}
<div class="mb-3">
{% include 'snippets/shelf_selector.html' with shelf=shelf.shelf class="is-small" readthrough=readthrough %}
</div>
</li>
{% endfor %}
</ul>
{% endif %}
{% for shelf in other_edition_shelves %}
<p>
{% blocktrans with book_path=shelf.book.local_path shelf_path=shelf.shelf.local_path shelf_name=shelf.shelf.name %}A <a href="{{ book_path }}">different edition</a> of this book is on your <a href="{{ shelf_path }}">{{ shelf_name }}</a> shelf.{% endblocktrans %}

View File

@ -119,10 +119,16 @@
</fieldset>
{% endif %}
<div class="field">
<label class="label" for="id_add_author">{% trans "Add Authors:" %}</label>
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'John Doe, Jane Smith' %}" value="{{ add_author }}" {% if confirm_mode %}readonly{% endif %}>
<span class="help">{% trans "Separate multiple values with commas." %}</span>
<label class="label">{% trans "Add Authors:" %}</label>
{% for author in add_author %}
<label class="label is-sr-only" for="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}">{% trans "Add Author" %}</label>
<input class="input" type="text" name="add_author" id="id_add_author{% if not forloop.first %}-{{forloop.counter}}{% endif %}" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
{% empty %}
<label class="label is-sr-only" for="id_add_author">{% trans "Add Author" %}</label>
<input class="input" type="text" name="add_author" id="id_add_author" placeholder="{% trans 'Jane Doe' %}" value="{{ author }}" {% if confirm_mode %}readonly{% endif %}>
{% endfor %}
</div>
<span class="help"><button class="button is-small" type="button" data-duplicate="id_add_author" id="another_author_field">{% trans "Add Another Author" %}</button></span>
</div>
</section>
</div>

View File

@ -9,10 +9,11 @@ Finish "<em>{{ book_title }}</em>"
{% endblock %}
{% block modal-form-open %}
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" class="submit-status">
<form name="finish-reading" action="{% url 'reading-status' 'finish' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
{% csrf_token %}
<input type="hidden" name="id" value="{{ readthrough.id }}">
<input type="hidden" name="reading_status" value="read">
<input type="hidden" name="shelf" value="{{ move_from }}">
{% endblock %}
{% block reading-dates %}

View File

@ -9,8 +9,9 @@ Start "<em>{{ book_title }}</em>"
{% endblock %}
{% block modal-form-open %}
<form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post" class="submit-status">
<form name="start-reading" action="{% url 'reading-status' 'start' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
<input type="hidden" name="reading_status" value="reading">
<input type="hidden" name="shelf" value="{{ move_from }}">
{% csrf_token %}
{% endblock %}

View File

@ -9,8 +9,9 @@ Want to Read "<em>{{ book_title }}</em>"
{% endblock %}
{% block modal-form-open %}
<form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post" class="submit-status">
<form name="shelve" action="{% url 'reading-status' 'want' book.id %}" method="post" {% if not refresh %}class="submit-status"{% endif %}>
<input type="hidden" name="reading_status" value="to-read">
<input type="hidden" name="shelf" value="{{ move_from }}">
{% csrf_token %}
{% endblock %}

View File

@ -1,29 +1,88 @@
{% extends 'components/dropdown.html' %}
{% load i18n %}
{% load bookwyrm_tags %}
{% load utilities %}
{% block dropdown-trigger %}
<span>{% trans "Move book" %}</span>
<span class="icon icon-arrow-down" aria-hidden="true"></span>
{% endblock %}
{% block dropdown-list %}
{% with book.id|uuid as uuid %}
{% active_shelf book as active_shelf %}
{% latest_read_through book request.user as readthrough %}
{% for shelf in user_shelves %}
{% if shelf.editable %}
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/shelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="change-shelf-from" value="{{ current.identifier }}">
<input type="hidden" name="shelf" value="{{ shelf.identifier }}">
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf.identifier == current.identifier %}disabled{% endif %}><span>{{ shelf.name }}</span></button>
<button class="button is-fullwidth is-small shelf-option is-radiusless is-white" type="submit" {% if shelf in book.shelf_set.all %} disabled {% endif %}><span>{{ shelf.name }}</span></button>
</form>
</li>
{% else%}
{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %}
{% with button_class="is-fullwidth is-small shelf-option is-radiusless is-white" %}
<li role="menuitem" class="dropdown-item p-0">
{% if shelf.identifier == 'reading' %}
{% trans "Start reading" as button_text %}
{% url 'reading-status' 'start' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="start_reading" controls_uid=uuid focus="modal_title_start_reading" disabled=is_current fallback_url=fallback_url %}
{% elif shelf.identifier == 'read' %}
{% trans "Read" as button_text %}
{% url 'reading-status' 'finish' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="finish_reading" controls_uid=uuid focus="modal_title_finish_reading" disabled=is_current fallback_url=fallback_url %}
{% elif shelf.identifier == 'to-read' %}
{% trans "Want to read" as button_text %}
{% url 'reading-status' 'want' book.id as fallback_url %}
{% include 'snippets/toggle/toggle_button.html' with class=button_class text=button_text controls_text="want_to_read" controls_uid=uuid focus="modal_title_want_to_read" disabled=is_current fallback_url=fallback_url %}
{% endif %}
</li>
{% endwith %}
{% endif %}
{% endfor %}
<li class="navbar-divider" role="separator"></li>
{% if shelf.identifier == 'all' %}
{% for shelved_in in book.shelves.all %}
<li class="navbar-divider m-0" role="separator" ></li>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ current.id }}">
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove" %}</button>
<input type="hidden" name="shelf" value="{{ shelved_in.id }}">
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ shelved_in.name }}</button>
</form>
</li>
{% endfor %}
{% else %}
<li class="navbar-divider" role="separator" ></li>
<li role="menuitem" class="dropdown-item p-0">
<form name="shelve" action="/unshelve/" method="post">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="shelf" value="{{ shelf.id }}">
<button class="button is-fullwidth is-small is-radiusless is-danger is-light" type="submit">{% trans "Remove from" %} {{ shelf.name }}</button>
</form>
</li>
{% endif %}
{% include 'snippets/reading_modals/want_to_read_modal.html' with book=active_shelf.book controls_text="want_to_read" controls_uid=uuid move_from=current.id refresh=True %}
{% include 'snippets/reading_modals/start_reading_modal.html' with book=active_shelf.book controls_text="start_reading" controls_uid=uuid move_from=current.id refresh=True %}
{% include 'snippets/reading_modals/finish_reading_modal.html' with book=active_shelf.book controls_text="finish_reading" controls_uid=uuid move_from=current.id readthrough=readthrough refresh=True %}
{% endwith %}
{% endblock %}

View File

@ -32,7 +32,7 @@
{% elif shelf.editable %}
<form name="shelve" action="/shelve/" method="post">
<form name="shelve" action="/shelve/" method="post" autocomplete="off">
{% csrf_token %}
<input type="hidden" name="book" value="{{ active_shelf.book.id }}">
<button class="button {{ class }}" name="shelf" type="submit" value="{{ shelf.identifier }}" {% if shelf in book.shelf_set.all %} disabled {% endif %}>

View File

@ -1,6 +1,6 @@
{% load utilities %}
{% if fallback_url %}
<form name="fallback_form_{{ 0|uuid }}" method="GET" action="{{ fallback_url }}">
<form name="fallback_form_{{ 0|uuid }}" method="GET" action="{{ fallback_url }}" autocomplete="off">
{% endif %}
<button
{% if not fallback_url %}

View File

@ -77,7 +77,12 @@ def related_status(notification):
def active_shelf(context, book):
"""check what shelf a user has a book on, if any"""
if hasattr(book, "current_shelves"):
return book.current_shelves[0] if len(book.current_shelves) else {"book": book}
read_shelves = [
s
for s in book.current_shelves
if s.shelf.identifier in models.Shelf.READ_STATUS_IDENTIFIERS
]
return read_shelves[0] if len(read_shelves) else {"book": book}
shelf = (
models.ShelfBook.objects.filter(

View File

@ -19,7 +19,7 @@ from django.utils import timezone
from bookwyrm import activitypub
from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status
from bookwyrm.models import fields, User, Status, Edition
from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN
@ -215,7 +215,7 @@ class ModelFields(TestCase):
"rat", "rat@rat.rat", "ratword", local=True, localname="rat"
)
public = "https://www.w3.org/ns/activitystreams#Public"
followers = "%s/followers" % user.remote_id
followers = f"{user.remote_id}/followers"
instance = fields.PrivacyField()
instance.name = "privacy_field"
@ -409,11 +409,10 @@ class ModelFields(TestCase):
"""loadin' a list of items from Links"""
# TODO
@responses.activate
@patch("bookwyrm.models.activitypub_mixin.ObjectMixin.broadcast")
@patch("bookwyrm.suggested_users.remove_user_task.delay")
def test_image_field(self, *_):
"""storing images"""
def test_image_field_to_activity(self, *_):
"""serialize an image field to activitypub"""
user = User.objects.create_user(
"mouse", "mouse@mouse.mouse", "mouseword", local=True, localname="mouse"
)
@ -437,16 +436,155 @@ class ModelFields(TestCase):
self.assertEqual(output.name, "")
self.assertEqual(output.type, "Document")
@responses.activate
def test_image_field_from_activity(self, *_):
"""load an image from activitypub"""
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)
instance = fields.ImageField()
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
body=user.avatar.file.read(),
body=image.tobytes(),
status=200,
)
loaded_image = instance.field_from_activity("http://www.example.com/image.jpg")
self.assertIsInstance(loaded_image, list)
self.assertIsInstance(loaded_image[1], ContentFile)
@responses.activate
def test_image_field_set_field_from_activity(self, *_):
"""update a model instance from an activitypub object"""
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)
instance = fields.ImageField(activitypub_field="cover", name="cover")
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
body=image.tobytes(),
status=200,
)
book = Edition.objects.create(title="hello")
MockActivity = namedtuple("MockActivity", ("cover"))
mock_activity = MockActivity("http://www.example.com/image.jpg")
instance.set_field_from_activity(book, mock_activity)
self.assertIsNotNone(book.cover.name)
self.assertEqual(book.cover.size, 43200)
@responses.activate
def test_image_field_set_field_from_activity_no_overwrite_no_cover(self, *_):
"""update a model instance from an activitypub object"""
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)
instance = fields.ImageField(activitypub_field="cover", name="cover")
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
body=image.tobytes(),
status=200,
)
book = Edition.objects.create(title="hello")
MockActivity = namedtuple("MockActivity", ("cover"))
mock_activity = MockActivity("http://www.example.com/image.jpg")
instance.set_field_from_activity(book, mock_activity, overwrite=False)
self.assertIsNotNone(book.cover.name)
self.assertEqual(book.cover.size, 43200)
@responses.activate
def test_image_field_set_field_from_activity_no_overwrite_with_cover(self, *_):
"""update a model instance from an activitypub object"""
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)
another_image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/logo.png"
)
another_image = Image.open(another_image_file)
another_output = BytesIO()
another_image.save(another_output, format=another_image.format)
instance = fields.ImageField(activitypub_field="cover", name="cover")
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
body=another_image.tobytes(),
status=200,
)
book = Edition.objects.create(title="hello")
book.cover.save("test.jpg", ContentFile(output.getvalue()))
self.assertEqual(book.cover.size, 2136)
MockActivity = namedtuple("MockActivity", ("cover"))
mock_activity = MockActivity("http://www.example.com/image.jpg")
instance.set_field_from_activity(book, mock_activity, overwrite=False)
# same cover as before
self.assertEqual(book.cover.size, 2136)
@responses.activate
def test_image_field_set_field_from_activity_with_overwrite_with_cover(self, *_):
"""update a model instance from an activitypub object"""
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)
book = Edition.objects.create(title="hello")
book.cover.save("test.jpg", ContentFile(output.getvalue()))
self.assertEqual(book.cover.size, 2136)
another_image_file = pathlib.Path(__file__).parent.joinpath(
"../../static/images/logo.png"
)
another_image = Image.open(another_image_file)
another_output = BytesIO()
another_image.save(another_output, format=another_image.format)
instance = fields.ImageField(activitypub_field="cover", name="cover")
responses.add(
responses.GET,
"http://www.example.com/image.jpg",
body=another_image.tobytes(),
status=200,
)
MockActivity = namedtuple("MockActivity", ("cover"))
mock_activity = MockActivity("http://www.example.com/image.jpg")
instance.set_field_from_activity(book, mock_activity, overwrite=True)
# new cover
self.assertIsNotNone(book.cover.name)
self.assertEqual(book.cover.size, 376800)
def test_datetime_field(self, *_):
"""this one is pretty simple, it just has to use isoformat"""
instance = fields.DateTimeField()

View File

@ -51,13 +51,14 @@ class EditBook(View):
if not form.is_valid():
return TemplateResponse(request, "book/edit/edit_book.html", data)
add_author = request.POST.get("add_author")
# we're adding an author through a free text field
# 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.split(","):
for author in add_author:
if not author:
continue
# check for existing authors

View File

@ -9,6 +9,7 @@ from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import models
from bookwyrm.views.shelf.shelf_actions import unshelve
from .status import CreateStatus
from .helpers import get_edition, handle_reading_status, is_api_request
from .helpers import load_date_in_user_tz_as_utc
@ -16,6 +17,7 @@ from .helpers import load_date_in_user_tz_as_utc
@method_decorator(login_required, name="dispatch")
# pylint: disable=no-self-use
# pylint: disable=too-many-return-statements
class ReadingStatus(View):
"""consider reading a book"""
@ -89,8 +91,21 @@ class ReadingStatus(View):
privacy = request.POST.get("privacy")
handle_reading_status(request.user, desired_shelf, book, privacy)
# if the request includes a "shelf" value we are using the 'move' button
if bool(request.POST.get("shelf")):
# unshelve the existing shelf
this_shelf = request.POST.get("shelf")
if (
bool(current_status_shelfbook)
and int(this_shelf) != int(current_status_shelfbook.shelf.id)
and current_status_shelfbook.shelf.identifier
!= desired_shelf.identifier
):
return unshelve(request, book_id=book_id)
if is_api_request(request):
return HttpResponse()
return redirect(referer)

View File

@ -91,13 +91,13 @@ def shelve(request):
@login_required
@require_POST
def unshelve(request):
def unshelve(request, book_id=False):
"""remove a book from a user's shelf"""
book = get_object_or_404(models.Edition, id=request.POST.get("book"))
identity = book_id if book_id else request.POST.get("book")
book = get_object_or_404(models.Edition, id=identity)
shelf_book = get_object_or_404(
models.ShelfBook, book=book, shelf__id=request.POST["shelf"]
)
shelf_book.raise_not_deletable(request.user)
shelf_book.delete()
return redirect(request.headers.get("Referer", "/"))

View File

@ -54,6 +54,7 @@ class CreateStatus(View):
data = {"book": book}
return TemplateResponse(request, "compose.html", data)
# pylint: disable=too-many-branches
def post(self, request, status_type, existing_status_id=None):
"""create status of whatever type"""
created = not existing_status_id
@ -117,11 +118,12 @@ class CreateStatus(View):
status.save(created=created)
# update a readthorugh, if needed
try:
edit_readthrough(request)
except Http404:
pass
# update a readthrough, if needed
if bool(request.POST.get("id")):
try:
edit_readthrough(request)
except Http404:
pass
if is_api_request(request):
return HttpResponse()