forked from mirrors/bookwyrm
Merge branch 'main' into production
This commit is contained in:
commit
5446a5b238
22 changed files with 354 additions and 29 deletions
|
@ -4,11 +4,12 @@ import sys
|
||||||
|
|
||||||
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
from .base_activity import ActivityEncoder, Image, PublicKey, Signature
|
||||||
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
from .note import Note, GeneratedNote, Article, Comment, Review, Quotation
|
||||||
|
from .note import Tombstone
|
||||||
from .interaction import Boost, Like
|
from .interaction import Boost, Like
|
||||||
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
from .ordered_collection import OrderedCollection, OrderedCollectionPage
|
||||||
from .person import Person
|
from .person import Person
|
||||||
from .book import Edition, Work, Author
|
from .book import Edition, Work, Author
|
||||||
from .verbs import Create, Undo, Update
|
from .verbs import Create, Delete, Undo, Update
|
||||||
from .verbs import Follow, Accept, Reject
|
from .verbs import Follow, Accept, Reject
|
||||||
from .verbs import Add, Remove
|
from .verbs import Add, Remove
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,14 @@ from typing import Dict, List
|
||||||
|
|
||||||
from .base_activity import ActivityObject, Image
|
from .base_activity import ActivityObject, Image
|
||||||
|
|
||||||
|
@dataclass(init=False)
|
||||||
|
class Tombstone(ActivityObject):
|
||||||
|
url: str
|
||||||
|
published: str
|
||||||
|
deleted: str
|
||||||
|
type: str = 'Tombstone'
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Note(ActivityObject):
|
class Note(ActivityObject):
|
||||||
''' Note activity '''
|
''' Note activity '''
|
||||||
|
|
|
@ -21,6 +21,15 @@ class Create(Verb):
|
||||||
type: str = 'Create'
|
type: str = 'Create'
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(init=False)
|
||||||
|
class Delete(Verb):
|
||||||
|
''' Create activity '''
|
||||||
|
to: List
|
||||||
|
cc: List
|
||||||
|
signature: Signature
|
||||||
|
type: str = 'Delete'
|
||||||
|
|
||||||
|
|
||||||
@dataclass(init=False)
|
@dataclass(init=False)
|
||||||
class Update(Verb):
|
class Update(Verb):
|
||||||
''' Update activity '''
|
''' Update activity '''
|
||||||
|
|
|
@ -57,6 +57,7 @@ def shared_inbox(request):
|
||||||
'Accept': handle_follow_accept,
|
'Accept': handle_follow_accept,
|
||||||
'Reject': handle_follow_reject,
|
'Reject': handle_follow_reject,
|
||||||
'Create': handle_create,
|
'Create': handle_create,
|
||||||
|
'Delete': handle_delete_status,
|
||||||
'Like': handle_favorite,
|
'Like': handle_favorite,
|
||||||
'Announce': handle_boost,
|
'Announce': handle_boost,
|
||||||
'Add': {
|
'Add': {
|
||||||
|
@ -134,7 +135,9 @@ def handle_follow(activity):
|
||||||
except django.db.utils.IntegrityError as err:
|
except django.db.utils.IntegrityError as err:
|
||||||
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
|
if err.__cause__.diag.constraint_name != 'userfollowrequest_unique':
|
||||||
raise
|
raise
|
||||||
relationship = models.UserFollowRequest.objects.get(remote_id=activity['id'])
|
relationship = models.UserFollowRequest.objects.get(
|
||||||
|
remote_id=activity['id']
|
||||||
|
)
|
||||||
# send the accept normally for a duplicate request
|
# send the accept normally for a duplicate request
|
||||||
|
|
||||||
if not to_follow.manually_approves_followers:
|
if not to_follow.manually_approves_followers:
|
||||||
|
@ -143,7 +146,7 @@ def handle_follow(activity):
|
||||||
'FOLLOW',
|
'FOLLOW',
|
||||||
related_user=actor
|
related_user=actor
|
||||||
)
|
)
|
||||||
outgoing.handle_accept(actor, to_follow, relationship)
|
outgoing.handle_accept(relationship)
|
||||||
else:
|
else:
|
||||||
# Accept will be triggered manually
|
# Accept will be triggered manually
|
||||||
status_builder.create_notification(
|
status_builder.create_notification(
|
||||||
|
@ -194,7 +197,7 @@ def handle_follow_reject(activity):
|
||||||
user_object=rejecter
|
user_object=rejecter
|
||||||
)
|
)
|
||||||
request.delete()
|
request.delete()
|
||||||
#raises models.UserFollowRequest.DoesNotExist:
|
#raises models.UserFollowRequest.DoesNotExist
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
|
@ -235,6 +238,20 @@ def handle_create(activity):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.task
|
||||||
|
def handle_delete_status(activity):
|
||||||
|
''' remove a status '''
|
||||||
|
status_id = activity['object']['id']
|
||||||
|
try:
|
||||||
|
status = models.Status.objects.select_subclasses().get(
|
||||||
|
remote_id=status_id
|
||||||
|
)
|
||||||
|
except models.Status.DoesNotExist:
|
||||||
|
return
|
||||||
|
status_builder.delete_status(status)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@app.task
|
@app.task
|
||||||
def handle_favorite(activity):
|
def handle_favorite(activity):
|
||||||
''' approval of your good good post '''
|
''' approval of your good good post '''
|
||||||
|
|
24
bookwyrm/migrations/0053_auto_20201006_2020.py
Normal file
24
bookwyrm/migrations/0053_auto_20201006_2020.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-10-06 20:20
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookwyrm', '0052_auto_20201005_2145'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='status',
|
||||||
|
name='deleted',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='status',
|
||||||
|
name='deleted_date',
|
||||||
|
field=models.DateTimeField(default=django.utils.timezone.now),
|
||||||
|
),
|
||||||
|
]
|
18
bookwyrm/migrations/0054_auto_20201016_2359.py
Normal file
18
bookwyrm/migrations/0054_auto_20201016_2359.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# Generated by Django 3.0.7 on 2020-10-16 23:59
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bookwyrm', '0053_auto_20201006_2020'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='status',
|
||||||
|
name='deleted_date',
|
||||||
|
field=models.DateTimeField(),
|
||||||
|
),
|
||||||
|
]
|
|
@ -22,6 +22,8 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
sensitive = models.BooleanField(default=False)
|
sensitive = models.BooleanField(default=False)
|
||||||
# the created date can't be this, because of receiving federated posts
|
# the created date can't be this, because of receiving federated posts
|
||||||
published_date = models.DateTimeField(default=timezone.now)
|
published_date = models.DateTimeField(default=timezone.now)
|
||||||
|
deleted = models.BooleanField(default=False)
|
||||||
|
deleted_date = models.DateTimeField()
|
||||||
favorites = models.ManyToManyField(
|
favorites = models.ManyToManyField(
|
||||||
'User',
|
'User',
|
||||||
symmetrical=False,
|
symmetrical=False,
|
||||||
|
@ -104,6 +106,18 @@ class Status(OrderedCollectionPageMixin, BookWyrmModel):
|
||||||
**kwargs
|
**kwargs
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def to_activity(self, **kwargs):
|
||||||
|
''' return tombstone if the status is deleted '''
|
||||||
|
if self.deleted:
|
||||||
|
return activitypub.Tombstone(
|
||||||
|
id=self.remote_id,
|
||||||
|
url=self.remote_id,
|
||||||
|
deleted=http_date(self.deleted_date.timestamp()),
|
||||||
|
published=http_date(self.deleted_date.timestamp()),
|
||||||
|
).serialize()
|
||||||
|
return ActivitypubMixin.to_activity(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class GeneratedStatus(Status):
|
class GeneratedStatus(Status):
|
||||||
''' these are app-generated messages about user activity '''
|
''' these are app-generated messages about user activity '''
|
||||||
@property
|
@property
|
||||||
|
@ -112,7 +126,7 @@ class GeneratedStatus(Status):
|
||||||
message = self.content
|
message = self.content
|
||||||
books = ', '.join(
|
books = ', '.join(
|
||||||
'<a href="%s">"%s"</a>' % (self.book.local_id, self.book.title) \
|
'<a href="%s">"%s"</a>' % (self.book.local_id, self.book.title) \
|
||||||
for book in self.mention_books
|
for book in self.mention_books.all()
|
||||||
)
|
)
|
||||||
return '%s %s' % (message, books)
|
return '%s %s' % (message, books)
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@ from bookwyrm.status import create_review, create_status
|
||||||
from bookwyrm.status import create_quotation, create_comment
|
from bookwyrm.status import create_quotation, create_comment
|
||||||
from bookwyrm.status import create_tag, create_notification, create_rating
|
from bookwyrm.status import create_tag, create_notification, create_rating
|
||||||
from bookwyrm.status import create_generated_note
|
from bookwyrm.status import create_generated_note
|
||||||
|
from bookwyrm.status import delete_status
|
||||||
from bookwyrm.remote_user import get_or_create_remote_user
|
from bookwyrm.remote_user import get_or_create_remote_user
|
||||||
|
|
||||||
|
|
||||||
|
@ -80,8 +81,10 @@ def handle_unfollow(user, to_unfollow):
|
||||||
to_unfollow.followers.remove(user)
|
to_unfollow.followers.remove(user)
|
||||||
|
|
||||||
|
|
||||||
def handle_accept(user, to_follow, follow_request):
|
def handle_accept(follow_request):
|
||||||
''' send an acceptance message to a follow request '''
|
''' send an acceptance message to a follow request '''
|
||||||
|
user = follow_request.user_subject
|
||||||
|
to_follow = follow_request.user_object
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
relationship = models.UserFollows.from_request(follow_request)
|
relationship = models.UserFollows.from_request(follow_request)
|
||||||
follow_request.delete()
|
follow_request.delete()
|
||||||
|
@ -91,10 +94,12 @@ def handle_accept(user, to_follow, follow_request):
|
||||||
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
||||||
|
|
||||||
|
|
||||||
def handle_reject(user, to_follow, relationship):
|
def handle_reject(follow_request):
|
||||||
''' a local user who managed follows rejects a follow request '''
|
''' a local user who managed follows rejects a follow request '''
|
||||||
activity = relationship.to_reject_activity(user)
|
user = follow_request.user_subject
|
||||||
relationship.delete()
|
to_follow = follow_request.user_object
|
||||||
|
activity = follow_request.to_reject_activity()
|
||||||
|
follow_request.delete()
|
||||||
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
broadcast(to_follow, activity, privacy='direct', direct_recipients=[user])
|
||||||
|
|
||||||
|
|
||||||
|
@ -107,11 +112,16 @@ def handle_shelve(user, book, shelf):
|
||||||
broadcast(user, shelve.to_add_activity(user))
|
broadcast(user, shelve.to_add_activity(user))
|
||||||
|
|
||||||
# tell the world about this cool thing that happened
|
# tell the world about this cool thing that happened
|
||||||
|
try:
|
||||||
message = {
|
message = {
|
||||||
'to-read': 'wants to read',
|
'to-read': 'wants to read',
|
||||||
'reading': 'started reading',
|
'reading': 'started reading',
|
||||||
'read': 'finished reading'
|
'read': 'finished reading'
|
||||||
}[shelf.identifier]
|
}[shelf.identifier]
|
||||||
|
except KeyError:
|
||||||
|
# it's a non-standard shelf, don't worry about it
|
||||||
|
return
|
||||||
|
|
||||||
status = create_generated_note(user, message, mention_books=[book])
|
status = create_generated_note(user, message, mention_books=[book])
|
||||||
status.save()
|
status.save()
|
||||||
|
|
||||||
|
@ -193,6 +203,12 @@ def handle_import_books(user, items):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def handle_delete_status(user, status):
|
||||||
|
''' delete a status and broadcast deletion to other servers '''
|
||||||
|
delete_status(status)
|
||||||
|
broadcast(user, status.to_activity())
|
||||||
|
|
||||||
|
|
||||||
def handle_rate(user, book, rating):
|
def handle_rate(user, book, rating):
|
||||||
''' a review that's just a rating '''
|
''' a review that's just a rating '''
|
||||||
builder = create_rating
|
builder = create_rating
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
''' Handle user activity '''
|
''' Handle user activity '''
|
||||||
|
from datetime import datetime
|
||||||
from django.db import IntegrityError
|
from django.db import IntegrityError
|
||||||
|
|
||||||
from bookwyrm import models
|
from bookwyrm import models
|
||||||
|
@ -6,6 +7,12 @@ from bookwyrm.books_manager import get_or_create_book
|
||||||
from bookwyrm.sanitize_html import InputHtmlParser
|
from bookwyrm.sanitize_html import InputHtmlParser
|
||||||
|
|
||||||
|
|
||||||
|
def delete_status(status):
|
||||||
|
''' replace the status with a tombstone '''
|
||||||
|
status.deleted = True
|
||||||
|
status.deleted_date = datetime.now()
|
||||||
|
status.save()
|
||||||
|
|
||||||
def create_rating(user, book, rating):
|
def create_rating(user, book, rating):
|
||||||
''' a review that's just a rating '''
|
''' a review that's just a rating '''
|
||||||
if not rating or rating < 1 or rating > 5:
|
if not rating or rating < 1 or rating > 5:
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{% load humanize %}
|
{% load humanize %}
|
||||||
{% load fr_display %}
|
{% load fr_display %}
|
||||||
|
|
||||||
|
{% if not status.deleted %}
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<header class="card-header">
|
<header class="card-header">
|
||||||
{% include 'snippets/status_header.html' with status=status %}
|
{% include 'snippets/status_header.html' with status=status %}
|
||||||
|
@ -25,7 +26,29 @@
|
||||||
<span class="icon icon-public">
|
<span class="icon icon-public">
|
||||||
<span class="is-sr-only">Public post</span>
|
<span class="is-sr-only">Public post</span>
|
||||||
</span>
|
</span>
|
||||||
|
{% if status.user == request.user %}
|
||||||
|
<form name="delete-{{status.id}}" action="/delete-status" method="post">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="status" value="{{ status.id }}">
|
||||||
|
<button type="submit">
|
||||||
|
<span class="icon icon-cancel">
|
||||||
|
<span class="is-sr-only">Delete post</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a>
|
<a href="{{ status.remote_id }}">{{ status.published_date | naturaltime }}</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</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 %}
|
||||||
|
|
|
@ -42,7 +42,8 @@ def get_replies(status):
|
||||||
''' get all direct replies to a status '''
|
''' get all direct replies to a status '''
|
||||||
#TODO: this limit could cause problems
|
#TODO: this limit could cause problems
|
||||||
return models.Status.objects.filter(
|
return models.Status.objects.filter(
|
||||||
reply_parent=status
|
reply_parent=status,
|
||||||
|
deleted=False,
|
||||||
).select_subclasses().all()[:10]
|
).select_subclasses().all()[:10]
|
||||||
|
|
||||||
|
|
||||||
|
|
1
bookwyrm/tests/incoming/__init__.py
Normal file
1
bookwyrm/tests/incoming/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from . import *
|
|
@ -6,7 +6,6 @@ from bookwyrm import models, incoming
|
||||||
|
|
||||||
|
|
||||||
class Favorite(TestCase):
|
class Favorite(TestCase):
|
||||||
''' not too much going on in the books model but here we are '''
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.remote_user = models.User.objects.create_user(
|
self.remote_user = models.User.objects.create_user(
|
||||||
'rat', 'rat@rat.com', 'ratword',
|
'rat', 'rat@rat.com', 'ratword',
|
||||||
|
@ -25,7 +24,7 @@ class Favorite(TestCase):
|
||||||
)
|
)
|
||||||
|
|
||||||
datafile = pathlib.Path(__file__).parent.joinpath(
|
datafile = pathlib.Path(__file__).parent.joinpath(
|
||||||
'data/ap_user.json'
|
'../data/ap_user.json'
|
||||||
)
|
)
|
||||||
self.user_data = json.loads(datafile.read_bytes())
|
self.user_data = json.loads(datafile.read_bytes())
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import json
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models, incoming
|
from bookwyrm import models, incoming
|
||||||
|
|
||||||
|
|
||||||
class Follow(TestCase):
|
class IncomingFollow(TestCase):
|
||||||
''' not too much going on in the books model but here we are '''
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.remote_user = models.User.objects.create_user(
|
self.remote_user = models.User.objects.create_user(
|
||||||
'rat', 'rat@rat.com', 'ratword',
|
'rat', 'rat@rat.com', 'ratword',
|
|
@ -1,11 +1,9 @@
|
||||||
import json
|
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from bookwyrm import models, incoming
|
from bookwyrm import models, incoming
|
||||||
|
|
||||||
|
|
||||||
class IncomingFollowAccept(TestCase):
|
class IncomingFollowAccept(TestCase):
|
||||||
''' not too much going on in the books model but here we are '''
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.remote_user = models.User.objects.create_user(
|
self.remote_user = models.User.objects.create_user(
|
||||||
'rat', 'rat@rat.com', 'ratword',
|
'rat', 'rat@rat.com', 'ratword',
|
1
bookwyrm/tests/outgoing/__init__.py
Normal file
1
bookwyrm/tests/outgoing/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
from . import *
|
72
bookwyrm/tests/outgoing/test_follow.py
Normal file
72
bookwyrm/tests/outgoing/test_follow.py
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm import models, outgoing
|
||||||
|
|
||||||
|
|
||||||
|
class Following(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.remote_user = models.User.objects.create_user(
|
||||||
|
'rat', 'rat@rat.com', 'ratword',
|
||||||
|
local=False,
|
||||||
|
remote_id='https://example.com/users/rat',
|
||||||
|
inbox='https://example.com/users/rat/inbox',
|
||||||
|
outbox='https://example.com/users/rat/outbox',
|
||||||
|
)
|
||||||
|
self.local_user = models.User.objects.create_user(
|
||||||
|
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True,
|
||||||
|
remote_id='http://local.com/users/mouse',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_follow(self):
|
||||||
|
self.assertEqual(models.UserFollowRequest.objects.count(), 0)
|
||||||
|
|
||||||
|
outgoing.handle_follow(self.local_user, self.remote_user)
|
||||||
|
rel = models.UserFollowRequest.objects.get()
|
||||||
|
|
||||||
|
self.assertEqual(rel.user_subject, self.local_user)
|
||||||
|
self.assertEqual(rel.user_object, self.remote_user)
|
||||||
|
self.assertEqual(rel.status, 'follow_request')
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_unfollow(self):
|
||||||
|
self.remote_user.followers.add(self.local_user)
|
||||||
|
self.assertEqual(self.remote_user.followers.count(), 1)
|
||||||
|
outgoing.handle_unfollow(self.local_user, self.remote_user)
|
||||||
|
|
||||||
|
self.assertEqual(self.remote_user.followers.count(), 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_accept(self):
|
||||||
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
user_subject=self.local_user,
|
||||||
|
user_object=self.remote_user
|
||||||
|
)
|
||||||
|
rel_id = rel.id
|
||||||
|
|
||||||
|
outgoing.handle_accept(rel)
|
||||||
|
# request should be deleted
|
||||||
|
self.assertEqual(
|
||||||
|
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
|
||||||
|
)
|
||||||
|
# follow relationship should exist
|
||||||
|
self.assertEqual(self.remote_user.followers.first(), self.local_user)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_reject(self):
|
||||||
|
rel = models.UserFollowRequest.objects.create(
|
||||||
|
user_subject=self.local_user,
|
||||||
|
user_object=self.remote_user
|
||||||
|
)
|
||||||
|
rel_id = rel.id
|
||||||
|
|
||||||
|
outgoing.handle_reject(rel)
|
||||||
|
# request should be deleted
|
||||||
|
self.assertEqual(
|
||||||
|
models.UserFollowRequest.objects.filter(id=rel_id).count(), 0
|
||||||
|
)
|
||||||
|
# follow relationship should not exist
|
||||||
|
self.assertEqual(
|
||||||
|
models.UserFollows.objects.filter(id=rel_id).count(), 0
|
||||||
|
)
|
97
bookwyrm/tests/outgoing/test_shelving.py
Normal file
97
bookwyrm/tests/outgoing/test_shelving.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from django.test import TestCase
|
||||||
|
|
||||||
|
from bookwyrm import models, outgoing
|
||||||
|
|
||||||
|
|
||||||
|
class Shelving(TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.user = models.User.objects.create_user(
|
||||||
|
'mouse', 'mouse@mouse.com', 'mouseword',
|
||||||
|
local=True,
|
||||||
|
remote_id='http://local.com/users/mouse',
|
||||||
|
)
|
||||||
|
self.book = models.Edition.objects.create(
|
||||||
|
title='Example Edition',
|
||||||
|
remote_id='https://example.com/book/1',
|
||||||
|
)
|
||||||
|
self.shelf = models.Shelf.objects.create(
|
||||||
|
name='Test Shelf',
|
||||||
|
identifier='test-shelf',
|
||||||
|
user=self.user
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve(self):
|
||||||
|
outgoing.handle_shelve(self.user, self.book, self.shelf)
|
||||||
|
# make sure the book is on the shelf
|
||||||
|
self.assertEqual(self.shelf.books.get(), self.book)
|
||||||
|
|
||||||
|
|
||||||
|
def test_handle_shelve_to_read(self):
|
||||||
|
shelf = models.Shelf.objects.get(identifier='to-read')
|
||||||
|
|
||||||
|
outgoing.handle_shelve(self.user, self.book, shelf)
|
||||||
|
# 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.GeneratedStatus.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')
|
||||||
|
|
||||||
|
outgoing.handle_shelve(self.user, self.book, shelf)
|
||||||
|
# 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.GeneratedStatus.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')
|
||||||
|
|
||||||
|
outgoing.handle_shelve(self.user, self.book, shelf)
|
||||||
|
# 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.GeneratedStatus.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)
|
||||||
|
self.shelf.save()
|
||||||
|
self.assertEqual(self.shelf.books.count(), 1)
|
||||||
|
outgoing.handle_unshelve(self.user, self.book, self.shelf)
|
||||||
|
self.assertEqual(self.shelf.books.count(), 0)
|
|
@ -1,8 +1,6 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
import json
|
|
||||||
import pathlib
|
|
||||||
|
|
||||||
from bookwyrm import activitypub, models
|
from bookwyrm import models
|
||||||
from bookwyrm import status as status_builder
|
from bookwyrm import status as status_builder
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ localname_regex = r'(?P<username>[\w\-_]+)'
|
||||||
user_path = r'^user/%s' % username_regex
|
user_path = r'^user/%s' % username_regex
|
||||||
local_user_path = r'^user/%s' % localname_regex
|
local_user_path = r'^user/%s' % localname_regex
|
||||||
|
|
||||||
status_types = ['status', 'review', 'comment', 'quotation', 'boost']
|
status_types = ['status', 'review', 'comment', 'quotation', 'boost', 'generatedstatus']
|
||||||
status_path = r'%s/(%s)/(?P<status_id>\d+)' % \
|
status_path = r'%s/(%s)/(?P<status_id>\d+)' % \
|
||||||
(local_user_path, '|'.join(status_types))
|
(local_user_path, '|'.join(status_types))
|
||||||
|
|
||||||
|
@ -107,6 +107,8 @@ urlpatterns = [
|
||||||
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
re_path(r'^unfavorite/(?P<status_id>\d+)/?$', actions.unfavorite),
|
||||||
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
|
re_path(r'^boost/(?P<status_id>\d+)/?$', actions.boost),
|
||||||
|
|
||||||
|
re_path(r'^delete-status/?$', actions.delete_status),
|
||||||
|
|
||||||
re_path(r'^shelve/?$', actions.shelve),
|
re_path(r'^shelve/?$', actions.shelve),
|
||||||
|
|
||||||
re_path(r'^follow/?$', actions.follow),
|
re_path(r'^follow/?$', actions.follow),
|
||||||
|
|
|
@ -418,6 +418,27 @@ def boost(request, status_id):
|
||||||
outgoing.handle_boost(request.user, status)
|
outgoing.handle_boost(request.user, status)
|
||||||
return redirect(request.headers.get('Referer', '/'))
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
def delete_status(request):
|
||||||
|
''' delete and tombstone a status '''
|
||||||
|
status_id = request.POST.get('status')
|
||||||
|
if not status_id:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
try:
|
||||||
|
status = models.Status.objects.get(id=status_id)
|
||||||
|
except models.Status.DoesNotExist:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
# don't let people delete other people's statuses
|
||||||
|
if status.user != request.user:
|
||||||
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
|
# perform deletion
|
||||||
|
outgoing.handle_delete_status(request.user, status)
|
||||||
|
return redirect(request.headers.get('Referer', '/'))
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def follow(request):
|
def follow(request):
|
||||||
''' follow another user, here or abroad '''
|
''' follow another user, here or abroad '''
|
||||||
|
@ -473,7 +494,7 @@ def accept_follow_request(request):
|
||||||
# Request already dealt with.
|
# Request already dealt with.
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
outgoing.handle_accept(requester, request.user, follow_request)
|
outgoing.handle_accept(follow_request)
|
||||||
|
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
@ -495,7 +516,7 @@ def delete_follow_request(request):
|
||||||
except models.UserFollowRequest.DoesNotExist:
|
except models.UserFollowRequest.DoesNotExist:
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
outgoing.handle_reject(requester, request.user, follow_request)
|
outgoing.handle_reject(follow_request)
|
||||||
return redirect('/user/%s' % request.user.localname)
|
return redirect('/user/%s' % request.user.localname)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -118,7 +118,7 @@ def get_activity_feed(user, filter_level, model=models.Status):
|
||||||
|
|
||||||
activities = model
|
activities = model
|
||||||
if hasattr(model, 'objects'):
|
if hasattr(model, 'objects'):
|
||||||
activities = model.objects
|
activities = model.objects.filter(deleted=False)
|
||||||
|
|
||||||
activities = activities.order_by(
|
activities = activities.order_by(
|
||||||
'-created_date'
|
'-created_date'
|
||||||
|
|
Loading…
Reference in a new issue