Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-02-12 10:11:16 -08:00
commit 355b2fad35
13 changed files with 120 additions and 18 deletions

View file

@ -16,7 +16,7 @@ from .response import ActivitypubResponse
from .book import Edition, Work, Author from .book import Edition, Work, Author
from .verbs import Create, Delete, Undo, Update from .verbs import Create, Delete, Undo, Update
from .verbs import Follow, Accept, Reject, Block from .verbs import Follow, Accept, Reject, Block
from .verbs import Add, AddBook, Remove from .verbs import Add, AddBook, AddListItem, Remove
# this creates a list of all the Activity types that we can serialize, # this creates a list of all the Activity types that we can serialize,
# so when an Activity comes in from outside, we can check if it's known # so when an Activity comes in from outside, we can check if it's known

View file

@ -65,6 +65,13 @@ class ActivityObject:
def to_model(self, model, instance=None, save=True): def to_model(self, model, instance=None, save=True):
''' convert from an activity to a model instance ''' ''' convert from an activity to a model instance '''
if self.type != model.activity_serializer.type:
raise ActivitySerializerError(
'Wrong activity type "%s" for activity of type "%s"' % \
(model.activity_serializer.type,
self.type)
)
if not isinstance(self, model.activity_serializer): if not isinstance(self, model.activity_serializer):
raise ActivitySerializerError( raise ActivitySerializerError(
'Wrong activity type "%s" for model "%s" (expects "%s")' % \ 'Wrong activity type "%s" for model "%s" (expects "%s")' % \

View file

@ -70,17 +70,26 @@ class Reject(Verb):
@dataclass(init=False) @dataclass(init=False)
class Add(Verb): class Add(Verb):
'''Add activity ''' '''Add activity '''
target: ActivityObject target: str
object: ActivityObject
type: str = 'Add' type: str = 'Add'
@dataclass(init=False) @dataclass(init=False)
class AddBook(Verb): class AddBook(Add):
'''Add activity that's aware of the book obj ''' '''Add activity that's aware of the book obj '''
target: Edition object: Edition
type: str = 'Add' type: str = 'Add'
@dataclass(init=False)
class AddListItem(AddBook):
'''Add activity that's aware of the book obj '''
notes: str = None
order: int = 0
approved: bool = True
@dataclass(init=False) @dataclass(init=False)
class Remove(Verb): class Remove(Verb):
'''Remove activity ''' '''Remove activity '''

View file

@ -239,7 +239,8 @@ def get_image(url):
'User-Agent': settings.USER_AGENT, 'User-Agent': settings.USER_AGENT,
}, },
) )
except (RequestError, SSLError): except (RequestError, SSLError) as e:
logger.exception(e)
return None return None
if not resp.ok: if not resp.ok:
return None return None

View file

@ -142,7 +142,12 @@ class Connector(AbstractConnector):
work = book.parent_work work = book.parent_work
# we can mass download edition data from OL to avoid repeatedly querying # we can mass download edition data from OL to avoid repeatedly querying
edition_options = self.load_edition_data(work.openlibrary_key) try:
edition_options = self.load_edition_data(work.openlibrary_key)
except ConnectorException:
# who knows, man
return
for edition_data in edition_options.get('entries'): for edition_data in edition_options.get('entries'):
# does this edition have ANY interesting data? # does this edition have ANY interesting data?
if ignore_edition(edition_data): if ignore_edition(edition_data):

View file

@ -216,9 +216,9 @@ def handle_create_list(activity):
def handle_update_list(activity): def handle_update_list(activity):
''' update a list ''' ''' update a list '''
try: try:
book_list = models.List.objects.get(id=activity['object']['id']) book_list = models.List.objects.get(remote_id=activity['object']['id'])
except models.List.DoesNotExist: except models.List.DoesNotExist:
return book_list = None
activitypub.BookList( activitypub.BookList(
**activity['object']).to_model(models.List, instance=book_list) **activity['object']).to_model(models.List, instance=book_list)
@ -319,8 +319,19 @@ def handle_add(activity):
#this is janky as heck but I haven't thought of a better solution #this is janky as heck but I haven't thought of a better solution
try: try:
activitypub.AddBook(**activity).to_model(models.ShelfBook) activitypub.AddBook(**activity).to_model(models.ShelfBook)
return
except activitypub.ActivitySerializerError: except activitypub.ActivitySerializerError:
activitypub.AddBook(**activity).to_model(models.Tag) pass
try:
activitypub.AddListItem(**activity).to_model(models.ListItem)
return
except activitypub.ActivitySerializerError:
pass
try:
activitypub.AddBook(**activity).to_model(models.UserTag)
return
except activitypub.ActivitySerializerError:
pass
@app.task @app.task

View file

@ -10,7 +10,10 @@ def set_user(app_registry, schema_editor):
shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook') shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook')
for item in shelfbook.objects.using(db_alias).filter(user__isnull=True): for item in shelfbook.objects.using(db_alias).filter(user__isnull=True):
item.user = item.shelf.user item.user = item.shelf.user
item.save(broadcast=False) try:
item.save(broadcast=False)
except TypeError:
item.save()
class Migration(migrations.Migration): class Migration(migrations.Migration):

View file

@ -68,7 +68,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel):
order = fields.IntegerField(blank=True, null=True) order = fields.IntegerField(blank=True, null=True)
endorsement = models.ManyToManyField('User', related_name='endorsers') endorsement = models.ManyToManyField('User', related_name='endorsers')
activity_serializer = activitypub.AddBook activity_serializer = activitypub.AddListItem
object_field = 'book' object_field = 'book'
collection_field = 'book_list' collection_field = 'book_list'

View file

@ -103,7 +103,7 @@
<div class="column"> <div class="column">
<div class="block"> <div class="block">
<h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ review_count }} review{{ reviews|length|pluralize }})</h3> <h3 class="field is-grouped">{% include 'snippets/stars.html' with rating=rating %} ({{ review_count }} review{{ review_count|pluralize }})</h3>
{% include 'snippets/trimmed_text.html' with full=book|book_description %} {% include 'snippets/trimmed_text.html' with full=book|book_description %}

View file

@ -8,7 +8,7 @@
<a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span> <a href="{{ list.local_path }}">{{ list.name }}</a> <span class="subtitle">{% include 'snippets/privacy-icons.html' with item=list %}</span>
</h4> </h4>
</header> </header>
<div class="card-image is-flex"> <div class="card-image is-flex is-clipped">
{% for book in list.listitem_set.all|slice:5 %} {% for book in list.listitem_set.all|slice:5 %}
<a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a> <a href="{{ book.book.local_path }}">{% include 'snippets/book_cover.html' with book=book.book size="small" %}</a>
{% endfor %} {% endfor %}

View file

@ -1,10 +1,11 @@
{% load humanize %}
<p> <p>
{% if goal.progress_percent >= 100 %} {% if goal.progress_percent >= 100 %}
Success! Success!
{% elif goal.progress_percent %} {% elif goal.progress_percent %}
{{ goal.progress_percent }}% complete! {{ goal.progress_percent }}% complete!
{% endif %} {% endif %}
{% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}<a href="{{ goal.local_path }}">{% endif %}{{ goal.book_count }} of {{ goal.goal }} books{% if request.path != goal.local_path %}</a>{% endif %}. {% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}<a href="{{ goal.local_path }}">{% endif %}{{ goal.book_count }} of {{ goal.goal | intcomma }} books{% if request.path != goal.local_path %}</a>{% endif %}.
</p> </p>
<progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress> <progress class="progress is-large" value="{{ goal.book_count }}" max="{{ goal.goal }}" aria-hidden="true">{{ goal.progress_percent }}%</progress>

View file

@ -308,7 +308,7 @@ class Incoming(TestCase):
"@context": "https://www.w3.org/ns/activitystreams" "@context": "https://www.w3.org/ns/activitystreams"
} }
} }
incoming.handle_create_list(activity) incoming.handle_update_list(activity)
book_list.refresh_from_db() book_list.refresh_from_db()
self.assertEqual(book_list.name, 'Test List') self.assertEqual(book_list.name, 'Test List')
self.assertEqual(book_list.curation, 'curated') self.assertEqual(book_list.curation, 'curated')
@ -626,7 +626,7 @@ class Incoming(TestCase):
activity = { activity = {
"@context": "https://www.w3.org/ns/activitystreams", "@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159-aede-9f43c6b9314f", "id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block", "type": "Block",
"actor": "https://example.com/users/rat", "actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse" "object": "https://example.com/user/mouse"
@ -636,6 +636,29 @@ class Incoming(TestCase):
block = models.UserBlocks.objects.get() block = models.UserBlocks.objects.get()
self.assertEqual(block.user_subject, self.remote_user) self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user) self.assertEqual(block.user_object, self.local_user)
self.assertEqual(
block.remote_id, 'https://example.com/9e1f41ac-9ddd-4159')
self.assertFalse(models.UserFollows.objects.exists()) self.assertFalse(models.UserFollows.objects.exists())
self.assertFalse(models.UserFollowRequest.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists())
def test_handle_unblock(self):
''' unblock a user '''
self.remote_user.blocks.add(self.local_user)
block = models.UserBlocks.objects.get()
block.remote_id = 'https://example.com/9e1f41ac-9ddd-4159'
block.save()
self.assertEqual(block.user_subject, self.remote_user)
self.assertEqual(block.user_object, self.local_user)
activity = {'type': 'Undo', 'object': {
"@context": "https://www.w3.org/ns/activitystreams",
"id": "https://example.com/9e1f41ac-9ddd-4159",
"type": "Block",
"actor": "https://example.com/users/rat",
"object": "https://example.com/user/mouse"
}}
incoming.handle_unblock(activity)
self.assertFalse(models.UserBlocks.objects.exists())

View file

@ -3,7 +3,9 @@ import pathlib
from unittest.mock import patch from unittest.mock import patch
from PIL import Image from PIL import Image
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
@ -24,6 +26,8 @@ class UserViews(TestCase):
'rat@local.com', 'rat@rat.rat', 'password', 'rat@local.com', 'rat@rat.rat', 'password',
local=True, localname='rat') local=True, localname='rat')
models.SiteSettings.objects.create() models.SiteSettings.objects.create()
self.anonymous_user = AnonymousUser
self.anonymous_user.is_authenticated = False
def test_user_page(self): def test_user_page(self):
@ -38,6 +42,14 @@ class UserViews(TestCase):
result.render() result.render()
self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200)
request.user = self.anonymous_user
with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = False
result = view(request, 'mouse')
self.assertIsInstance(result, TemplateResponse)
result.render()
self.assertEqual(result.status_code, 200)
with patch('bookwyrm.views.user.is_api_request') as is_api: with patch('bookwyrm.views.user.is_api_request') as is_api:
is_api.return_value = True is_api.return_value = True
result = view(request, 'mouse') result = view(request, 'mouse')
@ -119,7 +131,7 @@ class UserViews(TestCase):
self.assertEqual(result.status_code, 404) self.assertEqual(result.status_code, 404)
def test_edit_profile_page(self): def test_edit_user_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.EditUser.as_view() view = views.EditUser.as_view()
request = self.factory.get('') request = self.factory.get('')
@ -135,12 +147,42 @@ class UserViews(TestCase):
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'
request = self.factory.post('', form.data) request = self.factory.post('', form.data)
request.user = self.local_user request.user = self.local_user
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): self.assertIsNone(self.local_user.name)
with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay') \
as delay_mock:
view(request) view(request)
self.assertEqual(delay_mock.call_count, 1)
self.assertEqual(self.local_user.name, 'New Name') self.assertEqual(self.local_user.name, 'New Name')
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
# 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))
def test_crop_avatar(self): def test_crop_avatar(self):