diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index 201e8042d..510f1f3f4 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -16,7 +16,7 @@ from .response import ActivitypubResponse from .book import Edition, Work, Author from .verbs import Create, Delete, Undo, Update 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, # so when an Activity comes in from outside, we can check if it's known diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 4bbb5e9f3..5f35f1d7e 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -65,6 +65,13 @@ class ActivityObject: def to_model(self, model, instance=None, save=True): ''' 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): raise ActivitySerializerError( 'Wrong activity type "%s" for model "%s" (expects "%s")' % \ diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 5502ced01..190cd7395 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -70,17 +70,26 @@ class Reject(Verb): @dataclass(init=False) class Add(Verb): '''Add activity ''' - target: ActivityObject + target: str + object: ActivityObject type: str = 'Add' @dataclass(init=False) -class AddBook(Verb): +class AddBook(Add): '''Add activity that's aware of the book obj ''' - target: Edition + object: Edition 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) class Remove(Verb): '''Remove activity ''' diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index 2cbcda47c..527d2f425 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -239,7 +239,8 @@ def get_image(url): 'User-Agent': settings.USER_AGENT, }, ) - except (RequestError, SSLError): + except (RequestError, SSLError) as e: + logger.exception(e) return None if not resp.ok: return None diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index cd196d274..a767a45ac 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -142,7 +142,12 @@ class Connector(AbstractConnector): work = book.parent_work # 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'): # does this edition have ANY interesting data? if ignore_edition(edition_data): diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index a88a748e4..18db10694 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -216,9 +216,9 @@ def handle_create_list(activity): def handle_update_list(activity): ''' update a list ''' 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: - return + book_list = None activitypub.BookList( **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 try: activitypub.AddBook(**activity).to_model(models.ShelfBook) + return 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 diff --git a/bookwyrm/migrations/0044_auto_20210207_1924.py b/bookwyrm/migrations/0044_auto_20210207_1924.py index 84b17055e..7289c73d8 100644 --- a/bookwyrm/migrations/0044_auto_20210207_1924.py +++ b/bookwyrm/migrations/0044_auto_20210207_1924.py @@ -10,7 +10,10 @@ def set_user(app_registry, schema_editor): shelfbook = app_registry.get_model('bookwyrm', 'ShelfBook') for item in shelfbook.objects.using(db_alias).filter(user__isnull=True): item.user = item.shelf.user - item.save(broadcast=False) + try: + item.save(broadcast=False) + except TypeError: + item.save() class Migration(migrations.Migration): diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index db8c1af6e..ef48ed956 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -68,7 +68,7 @@ class ListItem(CollectionItemMixin, BookWyrmModel): order = fields.IntegerField(blank=True, null=True) endorsement = models.ManyToManyField('User', related_name='endorsers') - activity_serializer = activitypub.AddBook + activity_serializer = activitypub.AddListItem object_field = 'book' collection_field = 'book_list' diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 1d8b5d35b..c174116e7 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -103,7 +103,7 @@
-

{% include 'snippets/stars.html' with rating=rating %} ({{ review_count }} review{{ reviews|length|pluralize }})

+

{% include 'snippets/stars.html' with rating=rating %} ({{ review_count }} review{{ review_count|pluralize }})

{% include 'snippets/trimmed_text.html' with full=book|book_description %} diff --git a/bookwyrm/templates/lists/list_items.html b/bookwyrm/templates/lists/list_items.html index 87b195edb..a487bbd6c 100644 --- a/bookwyrm/templates/lists/list_items.html +++ b/bookwyrm/templates/lists/list_items.html @@ -8,7 +8,7 @@ {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %} -
+
{% for book in list.listitem_set.all|slice:5 %} {% include 'snippets/book_cover.html' with book=book.book size="small" %} {% endfor %} diff --git a/bookwyrm/templates/snippets/goal_progress.html b/bookwyrm/templates/snippets/goal_progress.html index 997fbfbdf..43f27f4ee 100644 --- a/bookwyrm/templates/snippets/goal_progress.html +++ b/bookwyrm/templates/snippets/goal_progress.html @@ -1,10 +1,11 @@ +{% load humanize %}

{% if goal.progress_percent >= 100 %} Success! {% elif goal.progress_percent %} {{ goal.progress_percent }}% complete! {% endif %} - {% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}{% endif %}{{ goal.book_count }} of {{ goal.goal }} books{% if request.path != goal.local_path %}{% endif %}. + {% if goal.user == request.user %}You've{% else %}{{ goal.user.display_name }} has{% endif %} read {% if request.path != goal.local_path %}{% endif %}{{ goal.book_count }} of {{ goal.goal | intcomma }} books{% if request.path != goal.local_path %}{% endif %}.

diff --git a/bookwyrm/tests/test_incoming.py b/bookwyrm/tests/test_incoming.py index d84fbd8d6..01d0c9a39 100644 --- a/bookwyrm/tests/test_incoming.py +++ b/bookwyrm/tests/test_incoming.py @@ -308,7 +308,7 @@ class Incoming(TestCase): "@context": "https://www.w3.org/ns/activitystreams" } } - incoming.handle_create_list(activity) + incoming.handle_update_list(activity) book_list.refresh_from_db() self.assertEqual(book_list.name, 'Test List') self.assertEqual(book_list.curation, 'curated') @@ -626,7 +626,7 @@ class Incoming(TestCase): activity = { "@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", "actor": "https://example.com/users/rat", "object": "https://example.com/user/mouse" @@ -636,6 +636,29 @@ class Incoming(TestCase): block = models.UserBlocks.objects.get() self.assertEqual(block.user_subject, self.remote_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.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()) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index ea59f042d..f08c17e71 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -3,7 +3,9 @@ import pathlib from unittest.mock import patch from PIL import Image +from django.contrib.auth.models import AnonymousUser from django.core.files.base import ContentFile +from django.core.files.uploadedfile import SimpleUploadedFile from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -24,6 +26,8 @@ class UserViews(TestCase): 'rat@local.com', 'rat@rat.rat', 'password', local=True, localname='rat') models.SiteSettings.objects.create() + self.anonymous_user = AnonymousUser + self.anonymous_user.is_authenticated = False def test_user_page(self): @@ -38,6 +42,14 @@ class UserViews(TestCase): result.render() 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: is_api.return_value = True result = view(request, 'mouse') @@ -119,7 +131,7 @@ class UserViews(TestCase): 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 ''' view = views.EditUser.as_view() request = self.factory.get('') @@ -135,12 +147,42 @@ class UserViews(TestCase): view = views.EditUser.as_view() form = forms.EditUserForm(instance=self.local_user) form.data['name'] = 'New Name' + form.data['email'] = 'wow@email.com' request = self.factory.post('', form.data) 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) + self.assertEqual(delay_mock.call_count, 1) 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):