diff --git a/Dockerfile b/Dockerfile index f03d84dd1..7456996e6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +1,15 @@ -FROM python:3 -ENV PYTHONUNBUFFERED 1 +FROM python:3.9 + +ENV PYTHONUNBUFFERED 1 + RUN mkdir /app RUN mkdir /app/static RUN mkdir /app/images + WORKDIR /app + COPY requirements.txt /app/ RUN pip install -r requirements.txt + COPY ./bookwyrm /app COPY ./celerywyrm /app diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index 5f16906b7..ee4eefbdd 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -76,11 +76,12 @@ class ActivityObject: if not isinstance(self, model.activity_serializer): raise TypeError('Wrong activity type for model') - # check for an existing instance - try: - return model.objects.get(remote_id=self.id) - except model.DoesNotExist: - pass + # check for an existing instance, if we're not updating a known obj + if not instance: + try: + return model.objects.get(remote_id=self.id) + except model.DoesNotExist: + pass model_fields = [m.name for m in model._meta.get_fields()] mapped_fields = {} diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index a7823f43f..bcb021ca7 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -269,7 +269,12 @@ def handle_favorite(activity): @app.task def handle_unfavorite(activity): ''' approval of your good good post ''' - like = activitypub.Like(**activity['object']).to_model(models.Favorite) + try: + like = models.Favorite.objects.filter( + remote_id=activity['object']['id'] + ).first() + except models.Favorite.DoesNotExist: + return like.delete() @@ -294,7 +299,7 @@ def handle_unboost(activity): remote_id=activity['object']['id'] ).first() if boost: - status_builder.delete_status(boost) + boost.delete() @app.task diff --git a/bookwyrm/outgoing.py b/bookwyrm/outgoing.py index 1b21603e4..c0253a99a 100644 --- a/bookwyrm/outgoing.py +++ b/bookwyrm/outgoing.py @@ -299,7 +299,8 @@ def handle_unfavorite(user, status): # can't find that status, idk return - fav_activity = activitypub.Undo(actor=user, object=favorite) + fav_activity = favorite.to_undo_activity(user) + favorite.delete() broadcast(user, fav_activity, direct_recipients=[status.user]) @@ -319,6 +320,17 @@ def handle_boost(user, status): broadcast(user, boost_activity) +def handle_unboost(user, status): + ''' a user regrets boosting a status ''' + boost = models.Boost.objects.filter( + boosted_status=status, user=user + ).first() + activity = boost.to_undo_activity(user) + + boost.delete() + broadcast(user, activity) + + def handle_update_book(user, book): ''' broadcast the news about our book ''' broadcast(user, book.to_update_activity(user)) diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html index 7133e818d..57765bed1 100644 --- a/bookwyrm/templates/snippets/boost_button.html +++ b/bookwyrm/templates/snippets/boost_button.html @@ -1,6 +1,6 @@ {% load fr_display %} -{% with activity.id|uuid as uuid %} -
+{% with status.id|uuid as uuid %} + {% csrf_token %}
-
+ {% csrf_token %}
-
+ {% csrf_token %} -
- - {% endif %} - - - - -{% else %} -
-
-

+ {% if status.status_type == 'Boost' %} {% include 'snippets/avatar.html' with user=status.user %} {% include 'snippets/username.html' with user=status.user %} - deleted this status -

-
-
+ boosted + {% include 'snippets/status_body.html' with status=status|boosted_status %} + {% else %} + {% include 'snippets/status_body.html' with status=status %} + {% endif %} {% endif %} diff --git a/bookwyrm/templates/snippets/status_body.html b/bookwyrm/templates/snippets/status_body.html new file mode 100644 index 000000000..9eec95cde --- /dev/null +++ b/bookwyrm/templates/snippets/status_body.html @@ -0,0 +1,120 @@ +{% load fr_display %} +{% load humanize %} + +{% if not status.deleted %} +
+
+
+
+
+ {% include 'snippets/status_header.html' with status=status %} +
+
+
+
+ +
+ {% include 'snippets/status_content.html' with status=status %} +
+ + +
+{% else %} +
+
+

+ {% include 'snippets/avatar.html' with user=status.user %} + {% include 'snippets/username.html' with user=status.user %} + deleted this status +

+
+
+{% endif %} diff --git a/bookwyrm/tests/connectors/test_self_connector.py b/bookwyrm/tests/connectors/test_self_connector.py index 3dd4ac12c..b80ad202c 100644 --- a/bookwyrm/tests/connectors/test_self_connector.py +++ b/bookwyrm/tests/connectors/test_self_connector.py @@ -64,8 +64,10 @@ class SelfConnector(TestCase): def test_search_default_filter(self): - self.edition.default = True - self.edition.save() + ''' it should get rid of duplicate editions for the same work ''' + self.work.default_edition = self.edition + self.work.save() + results = self.connector.search('Anonymous') self.assertEqual(len(results), 1) self.assertEqual(results[0].title, 'Edition of Example Work') diff --git a/bookwyrm/tests/incoming/test_favorite.py b/bookwyrm/tests/incoming/test_favorite.py index eeba9000a..c528d38c6 100644 --- a/bookwyrm/tests/incoming/test_favorite.py +++ b/bookwyrm/tests/incoming/test_favorite.py @@ -17,6 +17,7 @@ class Favorite(TestCase): self.local_user = models.User.objects.create_user( 'mouse', 'mouse@mouse.com', 'mouseword', remote_id='http://local.com/user/mouse') + self.status = models.Status.objects.create( user=self.local_user, content='Test status', @@ -33,24 +34,13 @@ class Favorite(TestCase): def test_handle_favorite(self): activity = { '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'http://example.com/activity/1', - - 'type': 'Create', + 'id': 'http://example.com/fav/1', 'actor': 'https://example.com/users/rat', 'published': 'Mon, 25 May 2020 19:31:20 GMT', - 'to': ['https://example.com/user/rat/followers'], - 'cc': ['https://www.w3.org/ns/activitystreams#Public'], - 'object': { - '@context': 'https://www.w3.org/ns/activitystreams', - 'id': 'http://example.com/fav/1', - 'type': 'Like', - 'actor': 'https://example.com/users/rat', - 'object': 'http://local.com/status/1', - }, - 'signature': {} + 'object': 'http://local.com/status/1', } - result = incoming.handle_favorite(activity) + incoming.handle_favorite(activity) fav = models.Favorite.objects.get(remote_id='http://example.com/fav/1') self.assertEqual(fav.status, self.status) diff --git a/bookwyrm/tests/incoming/test_update_user.py b/bookwyrm/tests/incoming/test_update_user.py index 703078f16..7ac038eb5 100644 --- a/bookwyrm/tests/incoming/test_update_user.py +++ b/bookwyrm/tests/incoming/test_update_user.py @@ -1,3 +1,4 @@ +''' when a remote user changes their profile ''' import json import pathlib from django.test import TestCase diff --git a/bookwyrm/tests/models/test_book_model.py b/bookwyrm/tests/models/test_book_model.py index e8211a8f9..7dfad61f3 100644 --- a/bookwyrm/tests/models/test_book_model.py +++ b/bookwyrm/tests/models/test_book_model.py @@ -39,17 +39,8 @@ class Book(TestCase): title='Invalid Book' ) - def test_default_edition(self): - ''' a work should always be able to produce a deafult edition ''' - self.assertIsInstance(self.work.default_edition, models.Edition) - self.assertEqual(self.work.default_edition, self.first_edition) - - self.second_edition.default = True - self.second_edition.save() - - self.assertEqual(self.work.default_edition, self.second_edition) - def test_isbn_10_to_13(self): + ''' checksums and so on ''' isbn_10 = '178816167X' isbn_13 = isbn_10_to_13(isbn_10) self.assertEqual(isbn_13, '9781788161671') @@ -59,8 +50,8 @@ class Book(TestCase): self.assertEqual(isbn_13, '9781788161671') - def test_isbn_13_to_10(self): + ''' checksums and so on ''' isbn_13 = '9781788161671' isbn_10 = isbn_13_to_10(isbn_13) self.assertEqual(isbn_10, '178816167X') diff --git a/bookwyrm/tests/outgoing/test_shelving.py b/bookwyrm/tests/outgoing/test_shelving.py index cc2b6b170..0b85b671b 100644 --- a/bookwyrm/tests/outgoing/test_shelving.py +++ b/bookwyrm/tests/outgoing/test_shelving.py @@ -38,16 +38,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.get() - self.assertEqual(status.content, 'wants to read') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should not create a read-through - self.assertEqual(models.ReadThrough.objects.count(), 0) - def test_handle_shelve_reading(self): shelf = models.Shelf.objects.get(identifier='reading') @@ -56,20 +46,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.order_by('-published_date').first() - self.assertEqual(status.content, 'started reading') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should create a read-through - readthrough = models.ReadThrough.objects.get() - self.assertEqual(readthrough.user, self.user) - self.assertEqual(readthrough.book.id, self.book.id) - self.assertIsNotNone(readthrough.start_date) - self.assertIsNone(readthrough.finish_date) - def test_handle_shelve_read(self): shelf = models.Shelf.objects.get(identifier='read') @@ -78,20 +54,6 @@ class Shelving(TestCase): # make sure the book is on the shelf self.assertEqual(shelf.books.get(), self.book) - # it should have posted a status about this - status = models.GeneratedNote.objects.order_by('-published_date').first() - self.assertEqual(status.content, 'finished reading') - self.assertEqual(status.user, self.user) - self.assertEqual(status.mention_books.count(), 1) - self.assertEqual(status.mention_books.first(), self.book) - - # and it should update the existing read-through - readthrough = models.ReadThrough.objects.get() - self.assertEqual(readthrough.user, self.user) - self.assertEqual(readthrough.book.id, self.book.id) - self.assertIsNotNone(readthrough.start_date) - self.assertIsNotNone(readthrough.finish_date) - def test_handle_unshelve(self): self.shelf.books.add(self.book) diff --git a/bookwyrm/tests/test_books_manager.py b/bookwyrm/tests/test_books_manager.py index 46186838e..039bdfc5b 100644 --- a/bookwyrm/tests/test_books_manager.py +++ b/bookwyrm/tests/test_books_manager.py @@ -15,6 +15,8 @@ class Book(TestCase): title='Example Edition', parent_work=self.work ) + self.work.default_edition = self.edition + self.work.save() self.connector = models.Connector.objects.create( identifier='test_connector', diff --git a/bookwyrm/tests/test_signing.py b/bookwyrm/tests/test_signing.py index ed5600ac9..8653823d3 100644 --- a/bookwyrm/tests/test_signing.py +++ b/bookwyrm/tests/test_signing.py @@ -39,6 +39,7 @@ class Signature(TestCase): ) def send(self, signature, now, data, digest): + ''' test request ''' c = Client() return c.post( urlsplit(self.rat.inbox).path, @@ -73,13 +74,13 @@ class Signature(TestCase): def test_wrong_signature(self): ''' Messages must be signed by the right actor. - (cat cannot sign messages on behalf of mouse) - ''' + (cat cannot sign messages on behalf of mouse) ''' response = self.send_test_request(sender=self.mouse, signer=self.cat) self.assertEqual(response.status_code, 401) @responses.activate def test_remote_signer(self): + ''' signtures for remote users ''' datafile = pathlib.Path(__file__).parent.joinpath('data/ap_user.json') data = json.loads(datafile.read_bytes()) data['id'] = self.fake_remote.remote_id @@ -138,7 +139,6 @@ class Signature(TestCase): json=data, status=200) - # Key correct: response = self.send_test_request(sender=self.fake_remote) self.assertEqual(response.status_code, 200) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index f761eac91..6399c87e2 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -116,6 +116,7 @@ urlpatterns = [ re_path(r'^favorite/(?P\d+)/?$', actions.favorite), re_path(r'^unfavorite/(?P\d+)/?$', actions.unfavorite), re_path(r'^boost/(?P\d+)/?$', actions.boost), + re_path(r'^unboost/(?P\d+)/?$', actions.unboost), re_path(r'^delete-status/?$', actions.delete_status), diff --git a/bookwyrm/view_actions.py b/bookwyrm/view_actions.py index 653a9c23a..d938674de 100644 --- a/bookwyrm/view_actions.py +++ b/bookwyrm/view_actions.py @@ -508,6 +508,7 @@ def unfavorite(request, status_id): outgoing.handle_unfavorite(request.user, status) return redirect(request.headers.get('Referer', '/')) + @login_required def boost(request, status_id): ''' boost a status ''' @@ -516,6 +517,14 @@ def boost(request, status_id): return redirect(request.headers.get('Referer', '/')) +@login_required +def unboost(request, status_id): + ''' boost a status ''' + status = models.Status.objects.get(id=status_id) + outgoing.handle_unboost(request.user, status) + return redirect(request.headers.get('Referer', '/')) + + @login_required def delete_status(request): ''' delete and tombstone a status ''' diff --git a/docker-compose.yml b/docker-compose.yml index 0ece3bedc..a608c66fc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -71,7 +71,8 @@ services: - redis restart: on-failure flower: - image: mher/flower + build: . + command: flower --port=8888 env_file: .env environment: - CELERY_BROKER_URL=${CELERY_BROKER}