Merge branch 'main' into import-field-names

This commit is contained in:
Mouse Reeve 2021-11-11 14:08:16 -08:00
commit 915c41f59f
13 changed files with 118 additions and 91 deletions

View File

@ -36,7 +36,7 @@ FLOWER_PORT=8888
#FLOWER_USER=mouse #FLOWER_USER=mouse
#FLOWER_PASSWORD=changeme #FLOWER_PASSWORD=changeme
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123

View File

@ -36,7 +36,7 @@ FLOWER_PORT=8888
FLOWER_USER=mouse FLOWER_USER=mouse
FLOWER_PASSWORD=changeme FLOWER_PASSWORD=changeme
EMAIL_HOST="smtp.mailgun.org" EMAIL_HOST=smtp.mailgun.org
EMAIL_PORT=587 EMAIL_PORT=587
EMAIL_HOST_USER=mail@your.domain.here EMAIL_HOST_USER=mail@your.domain.here
EMAIL_HOST_PASSWORD=emailpassword123 EMAIL_HOST_PASSWORD=emailpassword123

View File

@ -3,6 +3,7 @@ from dataclasses import MISSING
import imghdr import imghdr
import re import re
from uuid import uuid4 from uuid import uuid4
from urllib.parse import urljoin
import dateutil.parser import dateutil.parser
from dateutil.parser import ParserError from dateutil.parser import ParserError
@ -13,11 +14,12 @@ from django.db import models
from django.forms import ClearableFileInput, ImageField as DjangoImageField from django.forms import ClearableFileInput, ImageField as DjangoImageField
from django.utils import timezone from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.encoding import filepath_to_uri
from bookwyrm import activitypub from bookwyrm import activitypub
from bookwyrm.connectors import get_image from bookwyrm.connectors import get_image
from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.sanitize_html import InputHtmlParser
from bookwyrm.settings import DOMAIN from bookwyrm.settings import MEDIA_FULL_URL
def validate_remote_id(value): def validate_remote_id(value):
@ -381,17 +383,6 @@ class CustomImageField(DjangoImageField):
widget = ClearableFileInputWithWarning widget = ClearableFileInputWithWarning
def image_serializer(value, alt):
"""helper for serializing images"""
if value and hasattr(value, "url"):
url = value.url
else:
return None
if not url[:4] == "http":
url = f"https://{DOMAIN}{url}"
return activitypub.Document(url=url, name=alt)
class ImageField(ActivitypubFieldMixin, models.ImageField): class ImageField(ActivitypubFieldMixin, models.ImageField):
"""activitypub-aware image field""" """activitypub-aware image field"""
@ -424,7 +415,12 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
activity[key] = formatted activity[key] = formatted
def field_to_activity(self, value, alt=None): def field_to_activity(self, value, alt=None):
return image_serializer(value, alt) url = get_absolute_url(value)
if not url:
return None
return activitypub.Document(url=url, name=alt)
def field_from_activity(self, value): def field_from_activity(self, value):
image_slug = value image_slug = value
@ -461,6 +457,20 @@ class ImageField(ActivitypubFieldMixin, models.ImageField):
) )
def get_absolute_url(value):
"""returns an absolute URL for the image"""
name = getattr(value, "name")
if not name:
return None
url = filepath_to_uri(name)
if url is not None:
url = url.lstrip("/")
url = urljoin(MEDIA_FULL_URL, url)
return url
class DateTimeField(ActivitypubFieldMixin, models.DateTimeField): class DateTimeField(ActivitypubFieldMixin, models.DateTimeField):
"""activitypub-aware datetime field""" """activitypub-aware datetime field"""

View File

@ -19,7 +19,6 @@ from bookwyrm.settings import ENABLE_PREVIEW_IMAGES
from .activitypub_mixin import ActivitypubMixin, ActivityMixin from .activitypub_mixin import ActivitypubMixin, ActivityMixin
from .activitypub_mixin import OrderedCollectionPageMixin from .activitypub_mixin import OrderedCollectionPageMixin
from .base_model import BookWyrmModel from .base_model import BookWyrmModel
from .fields import image_serializer
from .readthrough import ProgressMode from .readthrough import ProgressMode
from . import fields from . import fields
@ -190,15 +189,24 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
if hasattr(activity, "name"): if hasattr(activity, "name"):
activity.name = self.pure_name activity.name = self.pure_name
activity.type = self.pure_type activity.type = self.pure_type
activity.attachment = [ books = [getattr(self, "book", None)] + list(self.mention_books.all())
image_serializer(b.cover, b.alt_text) if len(books) == 1 and books[0].preview_image:
for b in self.mention_books.all()[:4] covers = [
if b.cover activitypub.Document(
] url=fields.get_absolute_url(books[0].preview_image),
if hasattr(self, "book") and self.book.cover: name=books[0].alt_text,
activity.attachment.append( )
image_serializer(self.book.cover, self.book.alt_text) ]
) else:
covers = [
activitypub.Document(
url=fields.get_absolute_url(b.cover),
name=b.alt_text,
)
for b in books
if b and b.cover
]
activity.attachment = covers
return activity return activity
def to_activity(self, pure=False): # pylint: disable=arguments-differ def to_activity(self, pure=False): # pylint: disable=arguments-differ

View File

@ -1,10 +1,24 @@
{% load i18n %} {% load i18n %}
{% load utilities %} {% load utilities %}
{% with user_path=status.user.local_path username=status.user.display_name book_path=status.book.local_poth book_title=book|book_title %} {% with user_path=status.user.local_path username=status.user.display_name book_path=book.local_path book_title=book|book_title %}
{% if status.status_type == 'GeneratedNote' %} {% if status.status_type == 'GeneratedNote' %}
{{ status.content|safe }} {% if status.content == 'wants to read' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> wants to read <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% if status.content == 'finished reading' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> finished reading <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% if status.content == 'started reading' %}
{% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> started reading <a href="{{ book_path }}">{{ book_title }}</a>
{% endblocktrans %}
{% endif %}
{% elif status.status_type == 'Rating' %} {% elif status.status_type == 'Rating' %}
{% blocktrans trimmed %} {% blocktrans trimmed %}
<a href="{{ user_path}}">{{ username }}</a> rated <a href="{{ book_path }}">{{ book_title }}</a> <a href="{{ user_path}}">{{ username }}</a> rated <a href="{{ book_path }}">{{ book_title }}</a>

View File

@ -46,10 +46,10 @@
</label> </label>
</div> </div>
<div class="field"> <div class="field">
<label> <label class="label" for="privacy_import">
<span class="label">{% trans "Privacy setting for imported reviews:" %}</span> {% trans "Privacy setting for imported reviews:" %}
{% include 'snippets/privacy_select.html' with no_label=True %}
</label> </label>
{% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,18 +10,17 @@
<h1 class="title">{% trans "Import Status" %}</h1> <h1 class="title">{% trans "Import Status" %}</h1>
<a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a> <a href="{% url 'import' %}" class="has-text-weight-normal help subtitle is-link">{% trans "Back to imports" %}</a>
{% if task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %}
<dl> <dl>
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Import started:" %}</dt>
<dt class="has-text-weight-medium">{% trans "Import started:" %}</dt> <dd>{{ job.created_date | naturaltime }}</dd>
<dd class="ml-2">{{ job.created_date | naturaltime }}</dd>
</div>
{% if job.complete %} {% if job.complete %}
<div class="is-flex"> <dt class="is-pulled-left mr-5">{% trans "Import completed:" %}</dt>
<dt class="has-text-weight-medium">{% trans "Import completed:" %}</dt> <dd>{{ task.date_done | naturaltime }}</dd>
<dd class="ml-2">{{ task.date_done | naturaltime }}</dd>
</div>
{% elif task.failed %}
<div class="notification is-danger">{% trans "TASK FAILED" %}</div>
{% endif %} {% endif %}
</dl> </dl>
</div> </div>

View File

@ -22,6 +22,7 @@ from bookwyrm.activitypub.base_activity import ActivityObject
from bookwyrm.models import fields, User, Status from bookwyrm.models import fields, User, Status
from bookwyrm.models.base_model import BookWyrmModel from bookwyrm.models.base_model import BookWyrmModel
from bookwyrm.models.activitypub_mixin import ActivitypubMixin from bookwyrm.models.activitypub_mixin import ActivitypubMixin
from bookwyrm.settings import DOMAIN
# pylint: disable=too-many-public-methods # pylint: disable=too-many-public-methods
@patch("bookwyrm.suggested_users.rerank_suggestions_task.delay") @patch("bookwyrm.suggested_users.rerank_suggestions_task.delay")
@ -424,21 +425,18 @@ class ModelFields(TestCase):
image.save(output, format=image.format) image.save(output, format=image.format)
user.avatar.save("test.jpg", ContentFile(output.getvalue())) user.avatar.save("test.jpg", ContentFile(output.getvalue()))
output = fields.image_serializer(user.avatar, alt="alt text") instance = fields.ImageField()
output = instance.field_to_activity(user.avatar)
self.assertIsNotNone( self.assertIsNotNone(
re.match( re.match(
r".*\.jpg", fr"https:\/\/{DOMAIN}\/.*\.jpg",
output.url, output.url,
) )
) )
self.assertEqual(output.name, "alt text") self.assertEqual(output.name, "")
self.assertEqual(output.type, "Document") self.assertEqual(output.type, "Document")
instance = fields.ImageField()
output = fields.image_serializer(user.avatar, alt=None)
self.assertEqual(instance.field_to_activity(user.avatar), output)
responses.add( responses.add(
responses.GET, responses.GET,
"http://www.example.com/image.jpg", "http://www.example.com/image.jpg",
@ -449,15 +447,6 @@ class ModelFields(TestCase):
self.assertIsInstance(loaded_image, list) self.assertIsInstance(loaded_image, list)
self.assertIsInstance(loaded_image[1], ContentFile) self.assertIsInstance(loaded_image[1], ContentFile)
def test_image_serialize(self, *_):
"""make sure we're creating sensible image paths"""
ValueMock = namedtuple("ValueMock", ("url"))
value_mock = ValueMock("/images/fish.jpg")
result = fields.image_serializer(value_mock, "hello")
self.assertEqual(result.type, "Document")
self.assertEqual(result.url, "https://your.domain.here/images/fish.jpg")
self.assertEqual(result.name, "hello")
def test_datetime_field(self, *_): def test_datetime_field(self, *_):
"""this one is pretty simple, it just has to use isoformat""" """this one is pretty simple, it just has to use isoformat"""
instance = fields.DateTimeField() instance = fields.DateTimeField()

View File

@ -2,6 +2,7 @@
from unittest.mock import patch from unittest.mock import patch
from io import BytesIO from io import BytesIO
import pathlib import pathlib
import re
from django.http import Http404 from django.http import Http404
from django.core.files.base import ContentFile from django.core.files.base import ContentFile
@ -190,9 +191,11 @@ class Status(TestCase):
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, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -220,9 +223,11 @@ class Status(TestCase):
f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>', f'test content<p>(comment on <a href="{self.book.remote_id}">"Test Edition"</a>)</p>',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -257,9 +262,11 @@ class Status(TestCase):
f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content', f'a sickening sense <p>-- <a href="{self.book.remote_id}">"Test Edition"</a></p>test content',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -298,9 +305,11 @@ class Status(TestCase):
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -320,9 +329,11 @@ class Status(TestCase):
) )
self.assertEqual(activity["content"], "test content") self.assertEqual(activity["content"], "test content")
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")
@ -341,9 +352,11 @@ class Status(TestCase):
f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars', f'rated <em><a href="{self.book.remote_id}">{self.book.title}</a></em>: 3 stars',
) )
self.assertEqual(activity["attachment"][0].type, "Document") self.assertEqual(activity["attachment"][0].type, "Document")
self.assertEqual( self.assertTrue(
activity["attachment"][0].url, re.match(
f"https://{settings.DOMAIN}{self.book.cover.url}", r"https:\/\/your.domain.here\/images\/covers\/test_[A-z0-9]+.jpg",
activity["attachment"][0].url,
)
) )
self.assertEqual(activity["attachment"][0].name, "Test Edition") self.assertEqual(activity["attachment"][0].name, "Test Edition")

View File

@ -5,6 +5,7 @@ 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
from bookwyrm.tests.validate_html import validate_html
from bookwyrm import forms, models, views from bookwyrm import forms, models, views
@ -34,7 +35,7 @@ class ImportViews(TestCase):
request.user = self.local_user request.user = self.local_user
result = view(request) result = view(request)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_import_status(self): def test_import_status(self):
@ -47,7 +48,7 @@ class ImportViews(TestCase):
async_result.return_value = [] async_result.return_value = []
result = view(request, import_job.id) result = view(request, import_job.id)
self.assertIsInstance(result, TemplateResponse) self.assertIsInstance(result, TemplateResponse)
result.render() validate_html(result.render())
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
def test_start_import(self): def test_start_import(self):
@ -59,7 +60,10 @@ class ImportViews(TestCase):
form.data["include_reviews"] = False form.data["include_reviews"] = False
csv_file = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv") csv_file = pathlib.Path(__file__).parent.joinpath("../data/goodreads.csv")
form.data["csv_file"] = SimpleUploadedFile( form.data["csv_file"] = SimpleUploadedFile(
csv_file, open(csv_file, "rb").read(), content_type="text/csv" # pylint: disable=consider-using-with
csv_file,
open(csv_file, "rb").read(),
content_type="text/csv",
) )
request = self.factory.post("", form.data) request = self.factory.post("", form.data)

13
bw-dev
View File

@ -61,7 +61,7 @@ case "$CMD" in
up) up)
docker-compose up --build "$@" docker-compose up --build "$@"
;; ;;
run) service_ports_web)
docker-compose run --rm --service-ports web docker-compose run --rm --service-ports web
;; ;;
initdb) initdb)
@ -96,9 +96,6 @@ case "$CMD" in
restart_celery) restart_celery)
docker-compose restart celery_worker docker-compose restart celery_worker
;; ;;
test)
runweb coverage run --source='.' --omit="*/test*,celerywyrm*,bookwyrm/migrations/*" manage.py test "$@"
;;
pytest) pytest)
execweb pytest --no-cov-on-fail "$@" execweb pytest --no-cov-on-fail "$@"
;; ;;
@ -148,14 +145,11 @@ case "$CMD" in
runweb) runweb)
runweb "$@" runweb "$@"
;; ;;
rundb)
rundb "$@"
;;
*) *)
set +x # No need to echo echo set +x # No need to echo echo
echo "Unrecognised command. Try:" echo "Unrecognised command. Try:"
echo " up [container]" echo " up [container]"
echo " run" echo " service_ports_web"
echo " initdb" echo " initdb"
echo " resetdb" echo " resetdb"
echo " makemigrations [migration]" echo " makemigrations [migration]"
@ -164,10 +158,8 @@ case "$CMD" in
echo " shell" echo " shell"
echo " dbshell" echo " dbshell"
echo " restart_celery" echo " restart_celery"
echo " test [path]"
echo " pytest [path]" echo " pytest [path]"
echo " collectstatic" echo " collectstatic"
echo " add_locale [locale]"
echo " makemessages" echo " makemessages"
echo " compilemessages [locale]" echo " compilemessages [locale]"
echo " build" echo " build"
@ -180,6 +172,5 @@ case "$CMD" in
echo " copy_media_to_s3" echo " copy_media_to_s3"
echo " set_cors_to_s3 [cors file]" echo " set_cors_to_s3 [cors file]"
echo " runweb [command]" echo " runweb [command]"
echo " rundb [command]"
;; ;;
esac esac

View File

@ -27,7 +27,7 @@ server {
# #
# client_max_body_size 3M; # client_max_body_size 3M;
# #
# if ($host != "you-domain.com") { # if ($host != "your-domain.com") {
# return 301 $scheme://your-domain.com$request_uri; # return 301 $scheme://your-domain.com$request_uri;
# } # }
# #

View File

@ -20,7 +20,6 @@ django-storages==1.11.1
# Dev # Dev
black==21.4b0 black==21.4b0
coverage==5.1
pytest-django==4.1.0 pytest-django==4.1.0
pytest==6.1.2 pytest==6.1.2
pytest-cov==2.10.1 pytest-cov==2.10.1