Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2020-11-07 19:02:05 -08:00
commit 0574e36602
19 changed files with 194 additions and 223 deletions

View file

@ -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

View file

@ -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 = {}

View file

@ -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

View file

@ -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))

View file

@ -1,6 +1,6 @@
{% load fr_display %}
{% with activity.id|uuid as uuid %}
<form name="boost" action="/boost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% with status.id|uuid as uuid %}
<form name="boost" action="/boost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }}-{{ uuid }} {% if request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit">
<span class="icon icon-boost">
@ -8,7 +8,7 @@
</span>
</button>
</form>
<form name="unboost" action="/unboost/{{ activity.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
<form name="unboost" action="/unboost/{{ status.id }}" method="post" onsubmit="return interact(event)" class="boost-{{ status.id }}-{{ uuid }} active {% if not request.user|boosted:status %}hidden{% endif %}" data-id="boost-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small is-success" type="submit">
<span class="icon icon-boost">

View file

@ -23,7 +23,7 @@
{% endfor %}
</div>
{% endif %}
<textarea name="{% if type == 'quote' %}quote{% else %}content{% endif %}" class="textarea" id="id_content_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
<textarea name="{% if type == 'quote' %}quote{% else %}content{% endif %}" class="textarea" id="id_quote_{{ book.id }}_{{ type }}" placeholder="{{ placeholder }}" required></textarea>
</div>
{% if type == 'quote' %}

View file

@ -1,6 +1,6 @@
{% load fr_display %}
{% with activity.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% with status.id|uuid as uuid %}
<form name="favorite" action="/favorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }}-{{ uuid }} {% if request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-small" type="submit">
<span class="icon icon-heart">
@ -8,7 +8,7 @@
</span>
</button>
</form>
<form name="unfavorite" action="/unfavorite/{{ activity.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
<form name="unfavorite" action="/unfavorite/{{ status.id }}" method="POST" onsubmit="return interact(event)" class="fav-{{ status.id }}-{{ uuid }} active {% if not request.user|liked:status %}hidden{% endif %}" data-id="fav-{{ status.id }}-{{ uuid }}">
{% csrf_token %}
<button class="button is-success is-small" type="submit">
<span class="icon icon-heart">

View file

@ -1,142 +1,11 @@
{% load humanize %}
{% load fr_display %}
{% if not status.deleted %}
<div class="card">
<header class="card-header">
<div class="card-header-title">
<div class="columns">
<div class="column is-narrow">
{% if status.status_type == 'Boost' %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
boosted
</div>
<div class="column">
{% include 'snippets/status_header.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status_header.html' with status=status %}
{% endif %}
</div>
</div>
</div>
</header>
<div class="card-content">
{% if status.status_type == 'Boost' %}
{% include 'snippets/status_content.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status_content.html' with status=status %}
{% endif %}
</div>
<footer>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% if status.status_type == 'Boost' %}
{% include 'snippets/reply_form.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/reply_form.html' with status=status %}
{% endif %}
</div>
</div>
</div>
{% endif %}
<div class="card-footer">
<div class="card-footer-item">
{% if request.user.is_authenticated %}
<label class="button is-small" for="show-comment-{{ status.id }}">
<span class="icon icon-comment"><span class="is-sr-only">Comment</span></span>
</label>
{% if status.status_type == 'Boost' %}
{% include 'snippets/boost_button.html' with status=status|boosted_status %}
{% include 'snippets/fav_button.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/boost_button.html' with status=status %}
{% include 'snippets/fav_button.html' with status=status %}
{% endif %}
{% else %}
<a href="/login">
<span class="icon icon-comment">
<span class="is-sr-only">Comment</span>
</span>
<span class="icon icon-boost">
<span class="is-sr-only">Boost status</span>
</span>
<span class="icon icon-heart">
<span class="is-sr-only">Like status</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% if status.privacy == 'public' %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif status.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif status.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if status.user == request.user %}
<div class="card-footer-item">
<label class="button" for="more-info-{{ status.id }}">
<div class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
</div>
</label>
</div>
{% endif %}
</div>
<div>
<input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}">
<div class="toggle-content hidden card-footer">
{% if status.user == request.user %}
<div class="card-footer-item">
<form name="delete-{{status.id}}" action="/delete-status" method="post">
{% csrf_token %}
<input type="hidden" name="status" value="{{ status.id }}">
<button class="button is-danger" type="submit">
Delete post
</button>
</form>
</div>
{% endif %}
</div>
</div>
</footer>
</div>
{% else %}
<div class="card">
<header class="card-header">
<p>
{% if status.status_type == 'Boost' %}
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
deleted this status
</p>
</header>
</div>
boosted
{% include 'snippets/status_body.html' with status=status|boosted_status %}
{% else %}
{% include 'snippets/status_body.html' with status=status %}
{% endif %}
{% endif %}

View file

@ -0,0 +1,120 @@
{% load fr_display %}
{% load humanize %}
{% if not status.deleted %}
<div class="card">
<header class="card-header">
<div class="card-header-title">
<div class="columns">
<div class="column is-narrow">
{% include 'snippets/status_header.html' with status=status %}
</div>
</div>
</div>
</header>
<div class="card-content">
{% include 'snippets/status_content.html' with status=status %}
</div>
<footer>
{% if request.user.is_authenticated %}
<input class="toggle-control" type="checkbox" name="show-comment-{{ status.id }}" id="show-comment-{{ status.id }}">
<div class="toggle-content hidden">
<div class="card-footer">
<div class="card-footer-item">
{% include 'snippets/reply_form.html' with status=status %}
</div>
</div>
</div>
{% endif %}
<div class="card-footer">
<div class="card-footer-item">
{% if request.user.is_authenticated %}
<label class="button is-small" for="show-comment-{{ status.id }}">
<span class="icon icon-comment"><span class="is-sr-only">Comment</span></span>
</label>
{% include 'snippets/boost_button.html' with status=status %}
{% include 'snippets/fav_button.html' with status=status %}
{% else %}
<a href="/login">
<span class="icon icon-comment">
<span class="is-sr-only">Comment</span>
</span>
<span class="icon icon-boost">
<span class="is-sr-only">Boost status</span>
</span>
<span class="icon icon-heart">
<span class="is-sr-only">Like status</span>
</span>
</a>
{% endif %}
</div>
<div class="card-footer-item">
{% if status.privacy == 'public' %}
<span class="icon icon-globe">
<span class="is-sr-only">Public post</span>
</span>
{% elif status.privacy == 'unlisted' %}
<span class="icon icon-unlock">
<span class="is-sr-only">Unlisted post</span>
</span>
{% elif status.privacy == 'followers' %}
<span class="icon icon-lock">
<span class="is-sr-only">Followers-only post</span>
</span>
{% else %}
<span class="icon icon-envelope">
<span class="is-sr-only">Private post</span>
</span>
{% endif %}
</div>
<div class="card-footer-item">
<a href="{{ status.remote_id }}">{{ status.published_date | post_date }}</a>
</div>
{% if status.user == request.user %}
<div class="card-footer-item">
<label class="button" for="more-info-{{ status.id }}">
<div class="icon icon-dots-three">
<span class="is-sr-only">More options</span>
</div>
</label>
</div>
{% endif %}
</div>
<div>
<input class="toggle-control" type="checkbox" name="more-info-{{ status.id }}" id="more-info-{{ status.id }}">
<div class="toggle-content hidden card-footer">
{% if status.user == request.user %}
<div class="card-footer-item">
<form name="delete-{{status.id}}" action="/delete-status" method="post">
{% csrf_token %}
<input type="hidden" name="status" value="{{ status.id }}">
<button class="button is-danger" type="submit">
Delete post
</button>
</form>
</div>
{% endif %}
</div>
</div>
</footer>
</div>
{% else %}
<div class="card">
<header class="card-header">
<p>
{% include 'snippets/avatar.html' with user=status.user %}
{% include 'snippets/username.html' with user=status.user %}
deleted this status
</p>
</header>
</div>
{% endif %}

View file

@ -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')

View file

@ -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)

View file

@ -1,3 +1,4 @@
''' when a remote user changes their profile '''
import json
import pathlib
from django.test import TestCase

View file

@ -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')

View file

@ -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)

View file

@ -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',

View file

@ -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)

View file

@ -116,6 +116,7 @@ urlpatterns = [
re_path(r'^favorite/(?P<status_id>\d+)/?$', actions.favorite),
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
re_path(r'^unboost/(?P<status_id>\d+)/?$', actions.unboost),
re_path(r'^delete-status/?$', actions.delete_status),

View file

@ -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 '''

View file

@ -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}