Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-03-15 16:02:53 -07:00
commit d202bd1d1d
16 changed files with 121 additions and 31 deletions

View file

@ -9,5 +9,5 @@ class Image(ActivityObject):
url: str url: str
name: str = "" name: str = ""
type: str = "Image" type: str = "Document"
id: str = "" id: str = None

View file

@ -445,7 +445,11 @@ def unfurl_related_field(related_field, sort_field=None):
unfurl_related_field(i) for i in related_field.order_by(sort_field).all() unfurl_related_field(i) for i in related_field.order_by(sort_field).all()
] ]
if related_field.reverse_unfurl: if related_field.reverse_unfurl:
return related_field.field_to_activity() # if it's a one-to-one (key pair)
if hasattr(related_field, "field_to_activity"):
return related_field.field_to_activity()
# if it's one-to-many (attachments)
return related_field.to_activity()
return related_field.remote_id return related_field.remote_id

View file

@ -25,7 +25,11 @@ class Image(Attachment):
""" an image attachment """ """ an image attachment """
image = fields.ImageField( image = fields.ImageField(
upload_to="status/", null=True, blank=True, activitypub_field="url" upload_to="status/",
null=True,
blank=True,
activitypub_field="url",
alt_field="caption",
) )
caption = fields.TextField(null=True, blank=True, activitypub_field="name") caption = fields.TextField(null=True, blank=True, activitypub_field="name")

View file

@ -1,7 +1,7 @@
""" database schema for books and shelves """ """ database schema for books and shelves """
import re import re
from django.db import models from django.db import models, transaction
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
from bookwyrm import activitypub from bookwyrm import activitypub
@ -148,6 +148,15 @@ class Work(OrderedCollectionPageMixin, Book):
""" in case the default edition is not set """ """ in case the default edition is not set """
return self.default_edition or self.editions.order_by("-edition_rank").first() return self.default_edition or self.editions.order_by("-edition_rank").first()
@transaction.atomic()
def reset_default_edition(self):
""" sets a new default edition based on computed rank """
self.default_edition = None
# editions are re-ranked implicitly
self.save()
self.default_edition = self.get_default_edition()
self.save()
def to_edition_list(self, **kwargs): def to_edition_list(self, **kwargs):
""" an ordered collection of editions """ """ an ordered collection of editions """
return self.to_ordered_collection( return self.to_ordered_collection(
@ -200,9 +209,13 @@ class Edition(Book):
activity_serializer = activitypub.Edition activity_serializer = activitypub.Edition
name_field = "title" name_field = "title"
def get_rank(self): def get_rank(self, ignore_default=False):
""" calculate how complete the data is on this edition """ """ calculate how complete the data is on this edition """
if self.parent_work and self.parent_work.default_edition == self: if (
not ignore_default
and self.parent_work
and self.parent_work.default_edition == self
):
# default edition has the highest rank # default edition has the highest rank
return 20 return 20
rank = 0 rank = 0

View file

@ -5,6 +5,7 @@ import re
from django.apps import apps from django.apps import apps
from django.core.validators import MaxValueValidator, MinValueValidator from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models from django.db import models
from django.template.loader import get_template
from django.utils import timezone from django.utils import timezone
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -309,7 +310,8 @@ class ReviewRating(Review):
@property @property
def pure_content(self): def pure_content(self):
return 'Rated "{}": {:d} stars'.format(self.book.title, self.rating) template = get_template("snippets/generated_status/rating.html")
return template.render({"book": self.book, "rating": self.rating}).strip()
activity_serializer = activitypub.Rating activity_serializer = activitypub.Rating
pure_type = "Note" pure_type = "Note"

View file

@ -233,7 +233,7 @@
</section> </section>
{% endif %} {% endif %}
{% if lists.exists %} {% if lists.exists or request.user.list_set.exists %}
<section class="content block"> <section class="content block">
<h2 class="title is-5">{% trans "Lists" %}</h2> <h2 class="title is-5">{% trans "Lists" %}</h2>
<ul> <ul>
@ -241,6 +241,26 @@
<li><a href="{{ list.local_path }}">{{ list.name }}</a></li> <li><a href="{{ list.local_path }}">{{ list.name }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
{% if request.user.list_set.exists %}
<form name="list-add" method="post" action="{% url 'list-add-book' %}">
{% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}">
<label class="label" for="id_list">{% trans "Add to list" %}</label>
<div class="field has-addons">
<div class="select control">
<select name="list" id="id_list">
{% for list in user.list_set.all %}
<option value="{{ list.id }}">{{ list.name }}</option>
{% endfor %}
</select>
</div>
<div class="control">
<button type="submit" class="button is-link">{% trans "Add" %}</button>
</div>
</div>
</form>
{% endif %}
</section> </section>
{% endif %} {% endif %}
</div> </div>

View file

@ -6,7 +6,7 @@
{% block content %} {% block content %}
<div class="block"> <div class="block">
<h1 class="title">{% blocktrans with path=work.local_path work_title=work.title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1> <h1 class="title">{% blocktrans with work_path=work.local_path work_title=work.title %}Editions of <a href="{{ work_path }}">"{{ work_title }}"</a>{% endblocktrans %}</h1>
{% include 'snippets/book_tiles.html' with books=editions %} {% include 'snippets/book_tiles.html' with books=editions %}
</div> </div>

View file

@ -83,9 +83,10 @@
</div> </div>
<div class="column"> <div class="column">
<p>{% include 'snippets/book_titleby.html' with book=book %}</p> <p>{% include 'snippets/book_titleby.html' with book=book %}</p>
<form name="add-book" method="post" action="{% url 'list-add-book' list.id %}"> <form name="add-book" method="post" action="{% url 'list-add-book' %}">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="book" value="{{ book.id }}"> <input type="hidden" name="book" value="{{ book.id }}">
<input type="hidden" name="list" value="{{ list.id }}">
<button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}{% trans "Add" %}{% else %}{% trans "Suggest" %}{% endif %}</button> <button type="submit" class="button is-small is-link">{% if list.curation == 'open' or request.user == list.user %}{% trans "Add" %}{% else %}{% trans "Suggest" %}{% endif %}</button>
</form> </form>
</div> </div>

View file

@ -0,0 +1,3 @@
{% load i18n %}{% load humanize %}
{% blocktrans with title=book.title path=book.remote_id rating=rating count counter=rating %}Rated <em><a href="{{ path }}">{{ title }}</a></em>: {{ rating }} star{% plural %}Rated <em><a href="{{ path }}">{{ title }}</a></em>: {{ rating }} stars{% endblocktrans %}

View file

@ -123,7 +123,7 @@ class BaseActivity(TestCase):
summary="", summary="",
publicKey={"id": "hi", "owner": self.user.remote_id, "publicKeyPem": "hi"}, publicKey={"id": "hi", "owner": self.user.remote_id, "publicKeyPem": "hi"},
endpoints={}, endpoints={},
icon={"type": "Image", "url": "http://www.example.com/image.jpg"}, icon={"type": "Document", "url": "http://www.example.com/image.jpg"},
) )
responses.add( responses.add(
@ -194,7 +194,7 @@ class BaseActivity(TestCase):
{ {
"url": "http://www.example.com/image.jpg", "url": "http://www.example.com/image.jpg",
"name": "alt text", "name": "alt text",
"type": "Image", "type": "Document",
} }
], ],
) )
@ -224,7 +224,7 @@ class BaseActivity(TestCase):
data = { data = {
"url": "http://www.example.com/image.jpg", "url": "http://www.example.com/image.jpg",
"name": "alt text", "name": "alt text",
"type": "Image", "type": "Document",
} }
responses.add( responses.add(
responses.GET, responses.GET,

View file

@ -404,7 +404,7 @@ class ActivitypubFields(TestCase):
) )
) )
self.assertEqual(output.name, "alt text") self.assertEqual(output.name, "alt text")
self.assertEqual(output.type, "Image") self.assertEqual(output.type, "Document")
instance = fields.ImageField() instance = fields.ImageField()

View file

@ -169,7 +169,7 @@ class Status(TestCase):
self.assertEqual(activity["type"], "Note") self.assertEqual(activity["type"], "Note")
self.assertEqual(activity["sensitive"], False) self.assertEqual(activity["sensitive"], False)
self.assertIsInstance(activity["attachment"], list) self.assertIsInstance(activity["attachment"], list)
self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, activity["attachment"][0].url,
"https://%s%s" % (settings.DOMAIN, self.book.cover.url), "https://%s%s" % (settings.DOMAIN, self.book.cover.url),
@ -200,7 +200,7 @@ class Status(TestCase):
'test content<p>(comment on <a href="%s">"Test Edition"</a>)</p>' 'test content<p>(comment on <a href="%s">"Test Edition"</a>)</p>'
% self.book.remote_id, % self.book.remote_id,
) )
self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, activity["attachment"][0].url,
"https://%s%s" % (settings.DOMAIN, self.book.cover.url), "https://%s%s" % (settings.DOMAIN, self.book.cover.url),
@ -238,7 +238,7 @@ class Status(TestCase):
'a sickening sense <p>-- <a href="%s">"Test Edition"</a></p>' 'a sickening sense <p>-- <a href="%s">"Test Edition"</a></p>'
"test content" % self.book.remote_id, "test content" % self.book.remote_id,
) )
self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, activity["attachment"][0].url,
"https://%s%s" % (settings.DOMAIN, self.book.cover.url), "https://%s%s" % (settings.DOMAIN, self.book.cover.url),
@ -278,7 +278,7 @@ class Status(TestCase):
activity["name"], 'Review of "%s" (3 stars): Review name' % self.book.title activity["name"], 'Review of "%s" (3 stars): Review name' % self.book.title
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Image") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertEqual(
activity["attachment"][0].url, activity["attachment"][0].url,
"https://%s%s" % (settings.DOMAIN, self.book.cover.url), "https://%s%s" % (settings.DOMAIN, self.book.cover.url),

View file

@ -1,5 +1,10 @@
""" test for app action functionality """ """ test for app action functionality """
from io import BytesIO
from unittest.mock import patch from unittest.mock import patch
import pathlib
from PIL import Image
from django.core.files.base import ContentFile
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
@ -9,8 +14,8 @@ from bookwyrm import views
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
class FeedMessageViews(TestCase): class FeedViews(TestCase):
""" dms """ """ activity feed, statuses, dms """
def setUp(self): def setUp(self):
""" we need basic test data and mocks """ """ we need basic test data and mocks """
@ -59,6 +64,42 @@ class FeedMessageViews(TestCase):
self.assertIsInstance(result, ActivitypubResponse) self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_status_page_with_image(self):
""" there are so many views, this just makes sure it LOADS """
view = views.Status.as_view()
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)
with patch("bookwyrm.models.activitypub_mixin.broadcast_task.delay"):
status = models.Review.objects.create(
content="hi",
user=self.local_user,
book=self.book,
)
attachment = models.Image.objects.create(
status=status, caption="alt text here"
)
attachment.image.save("test.jpg", ContentFile(output.getvalue()))
request = self.factory.get("")
request.user = self.local_user
with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = False
result = view(request, "mouse", status.id)
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
with patch("bookwyrm.views.feed.is_api_request") as is_api:
is_api.return_value = True
result = view(request, "mouse", status.id)
self.assertIsInstance(result, ActivitypubResponse)
self.assertEqual(result.status_code, 200)
def test_replies_page(self): def test_replies_page(self):
""" there are so many views, this just makes sure it LOADS """ """ there are so many views, this just makes sure it LOADS """
view = views.Replies.as_view() view = views.Replies.as_view()

View file

@ -271,11 +271,12 @@ class ListViews(TestCase):
"", "",
{ {
"book": self.book.id, "book": self.book.id,
"list": self.list.id,
}, },
) )
request.user = self.local_user request.user = self.local_user
views.list.add_book(request, self.list.id) views.list.add_book(request)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user) self.assertEqual(item.user, self.local_user)
@ -300,11 +301,12 @@ class ListViews(TestCase):
"", "",
{ {
"book": self.book.id, "book": self.book.id,
"list": self.list.id,
}, },
) )
request.user = self.rat request.user = self.rat
views.list.add_book(request, self.list.id) views.list.add_book(request)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat) self.assertEqual(item.user, self.rat)
@ -330,11 +332,12 @@ class ListViews(TestCase):
"", "",
{ {
"book": self.book.id, "book": self.book.id,
"list": self.list.id,
}, },
) )
request.user = self.rat request.user = self.rat
views.list.add_book(request, self.list.id) views.list.add_book(request)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.rat) self.assertEqual(item.user, self.rat)
@ -360,11 +363,12 @@ class ListViews(TestCase):
"", "",
{ {
"book": self.book.id, "book": self.book.id,
"list": self.list.id,
}, },
) )
request.user = self.local_user request.user = self.local_user
views.list.add_book(request, self.list.id) views.list.add_book(request)
item = self.list.listitem_set.get() item = self.list.listitem_set.get()
self.assertEqual(item.book, self.book) self.assertEqual(item.book, self.book)
self.assertEqual(item.user, self.local_user) self.assertEqual(item.user, self.local_user)

View file

@ -117,9 +117,7 @@ urlpatterns = [
# lists # lists
re_path(r"^list/?$", views.Lists.as_view(), name="lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),
re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"), re_path(r"^list/(?P<list_id>\d+)(.json)?/?$", views.List.as_view(), name="list"),
re_path( re_path(r"^list/add-book/?$", views.list.add_book, name="list-add-book"),
r"^list/(?P<list_id>\d+)/add/?$", views.list.add_book, name="list-add-book"
),
re_path( re_path(
r"^list/(?P<list_id>\d+)/remove/?$", r"^list/(?P<list_id>\d+)/remove/?$",
views.list.remove_book, views.list.remove_book,

View file

@ -173,9 +173,9 @@ class Curate(View):
@require_POST @require_POST
def add_book(request, list_id): def add_book(request):
""" put a book on a list """ """ put a book on a list """
book_list = get_object_or_404(models.List, id=list_id) book_list = get_object_or_404(models.List, id=request.POST.get("list"))
if not object_visible_to_user(request.user, book_list): if not object_visible_to_user(request.user, book_list):
return HttpResponseNotFound() return HttpResponseNotFound()
@ -204,7 +204,7 @@ def add_book(request, list_id):
# if the book is already on the list, don't flip out # if the book is already on the list, don't flip out
pass pass
return redirect("list", list_id) return redirect("list", book_list.id)
@require_POST @require_POST