From 54f8a65ae2b114cdd40044a120ca8ab22a60b3a9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Thu, 21 Jan 2021 16:43:40 -0800 Subject: [PATCH 001/280] Adds block option to status menu --- bookwyrm/templates/snippets/status_body.html | 2 -- bookwyrm/templates/snippets/status_options.html | 12 ++++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/status_body.html b/bookwyrm/templates/snippets/status_body.html index 59ab234d..c70f898a 100644 --- a/bookwyrm/templates/snippets/status_body.html +++ b/bookwyrm/templates/snippets/status_body.html @@ -54,11 +54,9 @@ -{% if status.user == request.user %} -{% endif %} {% endblock %} diff --git a/bookwyrm/templates/snippets/status_options.html b/bookwyrm/templates/snippets/status_options.html index 6cd13dfd..9b312c7c 100644 --- a/bookwyrm/templates/snippets/status_options.html +++ b/bookwyrm/templates/snippets/status_options.html @@ -1,4 +1,5 @@ {% extends 'snippets/components/dropdown.html' %} +{% load bookwyrm_tags %} {% block dropdown-trigger %} @@ -7,6 +8,7 @@ {% endblock %} {% block dropdown-list %} +{% if status.user == request.user %}
  • +
  • +{% endif %} +
  • + +
  • {% endblock %} From 0fe2e7a3561d7463b9cb496e54e43310ab1022a1 Mon Sep 17 00:00:00 2001 From: Chad Nelson Date: Wed, 20 Jan 2021 17:15:15 -0500 Subject: [PATCH 002/280] First pass at basic RSS --- bookwyrm/templates/snippets/rss_content.html | 1 + bookwyrm/templates/snippets/rss_title.html | 15 +++++++ bookwyrm/tests/views/test_rss_feed.py | 44 ++++++++++++++++++++ bookwyrm/urls.py | 3 ++ bookwyrm/views/rss_feed.py | 29 +++++++++++++ 5 files changed, 92 insertions(+) create mode 100644 bookwyrm/templates/snippets/rss_content.html create mode 100644 bookwyrm/templates/snippets/rss_title.html create mode 100644 bookwyrm/tests/views/test_rss_feed.py create mode 100644 bookwyrm/views/rss_feed.py diff --git a/bookwyrm/templates/snippets/rss_content.html b/bookwyrm/templates/snippets/rss_content.html new file mode 100644 index 00000000..a72752d0 --- /dev/null +++ b/bookwyrm/templates/snippets/rss_content.html @@ -0,0 +1 @@ +{{ obj.pure_content | safe }} diff --git a/bookwyrm/templates/snippets/rss_title.html b/bookwyrm/templates/snippets/rss_title.html new file mode 100644 index 00000000..91b6d7e6 --- /dev/null +++ b/bookwyrm/templates/snippets/rss_title.html @@ -0,0 +1,15 @@ +{{ obj.user.username }}{% if obj.status_type == 'GeneratedNote' %} + {{ obj.content | safe }} +{% elif obj.status_type == 'Review' and not obj.name and not obj.content%} + rated +{% elif obj.status_type == 'Review' %} + reviewed +{% elif obj.status_type == 'Comment' %} + commented on +{% elif obj.status_type == 'Quotation' %} + quoted +{% endif %} +{% if obj.book %}{{ obj.book.title | safe}} +{% elif obj.mention_books %} +{{ obj.mention_books.first.title }} +{% endif %} \ No newline at end of file diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py new file mode 100644 index 00000000..2e71e96e --- /dev/null +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -0,0 +1,44 @@ +''' testing import ''' + +from unittest.mock import patch + +from django.test import RequestFactory, TestCase +import responses + +from bookwyrm import models +from bookwyrm.views import rss_feed +from bookwyrm.settings import DOMAIN + +class RssFeed(TestCase): + ''' rss feed behaves as expected ''' + def setUp(self): + self.user = models.User.objects.create_user( + 'rss_user', 'rss@test.rss', 'password', local=True) + + work = models.Work.objects.create(title='Test Work') + self.book = models.Edition.objects.create( + title='Example Edition', + remote_id='https://example.com/book/1', + parent_work=work + ) + + self.review = models.Review.objects.create( + name='Review name', content='test content', rating=3, + user=self.user, book=self.book) + + self.quote = models.Quotation.objects.create( + quote='a sickening sense', content='test content', + user=self.user, book=self.book) + + self.generatednote = models.GeneratedNote.objects.create( + content='test content', user=self.user) + + self.factory = RequestFactory() + + + def test_rss_feed(self): + request = self.factory.get('/user/rss_user/rss') + response = RssFeed(request) + self.assertEqual(response.status_code, 200) + self.assertEqual(False, True) + diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index ab134b1c..ea1d6252 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -3,7 +3,9 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, re_path + from bookwyrm import incoming, settings, views, wellknown +from bookwyrm.views.rss_feed import RssFeed from bookwyrm.utils import regex user_path = r'^user/(?P%s)' % regex.username @@ -75,6 +77,7 @@ urlpatterns = [ re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()), re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()), re_path(r'^edit-profile/?$', views.EditUser.as_view()), + re_path(r'%s/rss' % user_path, views.rss_feed.RssFeed()), # reading goals re_path(r'%s/goal/(?P\d{4})/?$' % user_path, views.Goal.as_view()), diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py new file mode 100644 index 00000000..d8de8fac --- /dev/null +++ b/bookwyrm/views/rss_feed.py @@ -0,0 +1,29 @@ +''' ''' + +from django.contrib.syndication.views import Feed +from django.urls import reverse +from bookwyrm.models.user import User +from .helpers import get_activity_feed, get_user_from_username + +class RssFeed(Feed): + + description_template = "snippets/rss_content.html" + title_template = "snippets/rss_title.html" + + def get_object(self, request, username): + return get_user_from_username(username) + + def link(self, obj): + return obj.local_path + + def title(self, obj): + return f"Status updates from {obj.username}" + + + def items(self, obj): + return get_activity_feed(obj, ['public', 'unlisted', 'followers']) + + + def item_link(self, item): + return item.local_path + From cc8888dea20bd520a0169549d2a1d0fd7469d4a1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Jan 2021 11:03:10 -0800 Subject: [PATCH 003/280] Adds incoming handler for blocking --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/verbs.py | 4 ++++ bookwyrm/incoming.py | 18 ++++++++++++++++++ bookwyrm/models/relationship.py | 2 +- bookwyrm/tests/test_incoming.py | 26 ++++++++++++++++++++++++++ 5 files changed, 50 insertions(+), 2 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index a4fef41e..a7439722 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -14,7 +14,7 @@ from .person import Person, PublicKey from .response import ActivitypubResponse from .book import Edition, Work, Author from .verbs import Create, Delete, Undo, Update -from .verbs import Follow, Accept, Reject +from .verbs import Follow, Accept, Reject, Block from .verbs import Add, AddBook, Remove # this creates a list of all the Activity types that we can serialize, diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 7c627927..6977ee8e 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -48,6 +48,10 @@ class Follow(Verb): ''' Follow activity ''' type: str = 'Follow' +@dataclass(init=False) +class Block(Verb): + ''' Block activity ''' + type: str = 'Block' @dataclass(init=False) class Accept(Verb): diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 9653c5d2..5d93756f 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -3,6 +3,7 @@ import json from urllib.parse import urldefrag import django.db.utils +from django.db.models import Q from django.http import HttpResponse from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.views.decorators.csrf import csrf_exempt @@ -51,6 +52,7 @@ def shared_inbox(request): 'Follow': handle_follow, 'Accept': handle_follow_accept, 'Reject': handle_follow_reject, + 'Block': handle_block, 'Create': handle_create, 'Delete': handle_delete_status, 'Like': handle_favorite, @@ -179,6 +181,22 @@ def handle_follow_reject(activity): request.delete() #raises models.UserFollowRequest.DoesNotExist +@app.task +def handle_block(activity): + ''' blocking a user ''' + # create "block" databse entry + block = activitypub.Block(**activity).to_model(models.UserBlocks) + + # remove follow relationships + models.UserFollows.objects.filter( + Q(user_subject=block.user_subject, user_object=block.user_object) | \ + Q(user_subject=block.user_object, user_object=block.user_subject) + ).delete() + models.UserFollowRequest.objects.filter( + Q(user_subject=block.user_subject, user_object=block.user_object) | \ + Q(user_subject=block.user_object, user_object=block.user_subject) + ).delete() + @app.task def handle_create(activity): diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 0f3c1dab..9ea75a8f 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -94,5 +94,5 @@ class UserFollowRequest(UserRelationship): class UserBlocks(UserRelationship): ''' prevent another user from following you and seeing your posts ''' - # TODO: not implemented status = 'blocks' + activity_serializer = activitypub.Block diff --git a/bookwyrm/tests/test_incoming.py b/bookwyrm/tests/test_incoming.py index 2cd4869e..024c8e25 100644 --- a/bookwyrm/tests/test_incoming.py +++ b/bookwyrm/tests/test_incoming.py @@ -540,3 +540,29 @@ class Incoming(TestCase): incoming.handle_update_work({'object': bookdata}) book = models.Work.objects.get(id=book.id) self.assertEqual(book.title, 'Piranesi') + + + def test_handle_blocks(self): + ''' create a "block" database entry from an activity ''' + self.local_user.followers.add(self.remote_user) + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user) + self.assertTrue(models.UserFollows.objects.exists()) + self.assertTrue(models.UserFollowRequest.objects.exists()) + + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.com/9e1f41ac-9ddd-4159-aede-9f43c6b9314f", + "type": "Block", + "actor": "https://example.com/users/rat", + "object": "https://example.com/user/mouse" + } + + incoming.handle_block(activity) + block = models.UserBlocks.objects.get() + self.assertEqual(block.user_subject, self.remote_user) + self.assertEqual(block.user_object, self.local_user) + + self.assertFalse(models.UserFollows.objects.exists()) + self.assertFalse(models.UserFollowRequest.objects.exists()) From 6cc29a6cf8f57c3fcd9312955276743d7a354a44 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 23 Jan 2021 11:40:41 -0800 Subject: [PATCH 004/280] Hide content from blocked users --- bookwyrm/tests/views/test_helpers.py | 60 ++++++++++++++++++++++++++++ bookwyrm/views/helpers.py | 10 +++++ 2 files changed, 70 insertions(+) diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index bd892896..5e42b378 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -248,3 +248,63 @@ class ViewsHelpers(TestCase): views.helpers.handle_reading_status( self.local_user, self.shelf, self.book, 'public') self.assertFalse(models.GeneratedNote.objects.exists()) + + def test_object_visible_to_user(self): + ''' does a user have permission to view an object ''' + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='public') + self.assertTrue( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Shelf.objects.create( + name='test', user=self.remote_user, privacy='unlisted') + self.assertTrue( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='followers') + self.assertFalse( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='direct') + self.assertFalse( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='direct') + obj.mention_users.add(self.local_user) + self.assertTrue( + views.helpers.object_visible_to_user(self.local_user, obj)) + + def test_object_visible_to_user_follower(self): + ''' what you can see if you follow a user ''' + self.remote_user.followers.add(self.local_user) + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='followers') + self.assertTrue( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='direct') + self.assertFalse( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='direct') + obj.mention_users.add(self.local_user) + self.assertTrue( + views.helpers.object_visible_to_user(self.local_user, obj)) + + def test_object_visible_to_user_blocked(self): + ''' you can't see it if they block you ''' + self.remote_user.blocks.add(self.local_user) + obj = models.Status.objects.create( + content='hi', user=self.remote_user, privacy='public') + self.assertFalse( + views.helpers.object_visible_to_user(self.local_user, obj)) + + obj = models.Shelf.objects.create( + name='test', user=self.remote_user, privacy='unlisted') + self.assertFalse( + views.helpers.object_visible_to_user(self.local_user, obj)) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 60159324..f899680f 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -38,11 +38,21 @@ def object_visible_to_user(viewer, obj): ''' is a user authorized to view an object? ''' if not obj: return False + + # viewer can't see it if the object's owner blocked them + if viewer in obj.user.blocks.all(): + return False + + # you can see your own posts and any public or unlisted posts if viewer == obj.user or obj.privacy in ['public', 'unlisted']: return True + + # you can see the followers only posts of people you follow if obj.privacy == 'followers' and \ obj.user.followers.filter(id=viewer.id).first(): return True + + # you can see dms you are tagged in if isinstance(obj, models.Status): if obj.privacy == 'direct' and \ obj.mention_users.filter(id=viewer.id).first(): From ad2baf3be514f9e9daac3a0849086327cd15a88e Mon Sep 17 00:00:00 2001 From: Chad Nelson Date: Sat, 23 Jan 2021 18:39:55 -0500 Subject: [PATCH 005/280] Use display_name instead of username --- bookwyrm/templates/snippets/rss_title.html | 2 +- bookwyrm/views/rss_feed.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/snippets/rss_title.html b/bookwyrm/templates/snippets/rss_title.html index 91b6d7e6..dadb5961 100644 --- a/bookwyrm/templates/snippets/rss_title.html +++ b/bookwyrm/templates/snippets/rss_title.html @@ -1,4 +1,4 @@ -{{ obj.user.username }}{% if obj.status_type == 'GeneratedNote' %} +{{ obj.user.display_name }}{% if obj.status_type == 'GeneratedNote' %} {{ obj.content | safe }} {% elif obj.status_type == 'Review' and not obj.name and not obj.content%} rated diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index d8de8fac..ee8063a6 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -17,11 +17,11 @@ class RssFeed(Feed): return obj.local_path def title(self, obj): - return f"Status updates from {obj.username}" + return f"Status updates from {obj.display_name}" def items(self, obj): - return get_activity_feed(obj, ['public', 'unlisted', 'followers']) + return get_activity_feed(obj, ['public', 'unlisted']) def item_link(self, item): From 4e0ec12052f26d289c3e0bb112492e5efcf617f0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 24 Jan 2021 16:13:26 -0800 Subject: [PATCH 006/280] hide blocked content from feed --- bookwyrm/templates/user.html | 3 ++- bookwyrm/tests/views/test_helpers.py | 28 ++++++++++++++++++++++++++++ bookwyrm/views/helpers.py | 5 +++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/user.html b/bookwyrm/templates/user.html index a41623a8..69b762b0 100644 --- a/bookwyrm/templates/user.html +++ b/bookwyrm/templates/user.html @@ -17,7 +17,7 @@ {% include 'snippets/user_header.html' with user=user %} - +{% if user.bookwyrm_user %}

    Shelves

    @@ -39,6 +39,7 @@
    See all {{ shelf_count }} shelves
    +{% endif %} {% if goal %}
    diff --git a/bookwyrm/tests/views/test_helpers.py b/bookwyrm/tests/views/test_helpers.py index 5e42b378..50c3cfc5 100644 --- a/bookwyrm/tests/views/test_helpers.py +++ b/bookwyrm/tests/views/test_helpers.py @@ -154,6 +154,34 @@ class ViewsHelpers(TestCase): self.assertEqual(statuses[0], rat_mention) + def test_get_activity_feed_blocks(self): + ''' feed generation with blocked users ''' + rat = models.User.objects.create_user( + 'rat', 'rat@rat.rat', 'password', local=True) + + public_status = models.Comment.objects.create( + content='public status', book=self.book, user=self.local_user) + rat_public = models.Status.objects.create( + content='blah blah', user=rat) + + statuses = views.helpers.get_activity_feed( + self.local_user, ['public']) + self.assertEqual(len(statuses), 2) + + # block relationship + rat.blocks.add(self.local_user) + statuses = views.helpers.get_activity_feed( + self.local_user, ['public']) + self.assertEqual(len(statuses), 1) + self.assertEqual(statuses[0], public_status) + + statuses = views.helpers.get_activity_feed( + rat, ['public']) + self.assertEqual(len(statuses), 1) + self.assertEqual(statuses[0], rat_public) + + + def test_is_bookwyrm_request(self): ''' checks if a request came from a bookwyrm instance ''' request = self.factory.get('', {'q': 'Test Book'}) diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index f899680f..b0f867b7 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -71,6 +71,11 @@ def get_activity_feed( # exclude deleted queryset = queryset.exclude(deleted=True).order_by('-published_date') + # exclude blocks from both directions + blocked = models.User.objects.filter(id__in=user.blocks.all()).all() + queryset = queryset.exclude( + Q(user__in=blocked) | Q(user__blocks=user)) + # you can't see followers only or direct messages if you're not logged in if user.is_anonymous: privacy = [p for p in privacy if not p in ['followers', 'direct']] From ed830323304a961397a47ef5fe648e15af540509 Mon Sep 17 00:00:00 2001 From: Joel Bradshaw Date: Sun, 24 Jan 2021 16:39:26 -0800 Subject: [PATCH 007/280] Fix migration for if db has multiple empty emails If the database has multiple users with an empty email column, this migration will fail because multiple empty strings break the unique constraint. A fresh database won't have this problem because it won't have any legacy users with empty strings instead of NULL, but for existing databases we need to convert the empty strings to NULL so they don't run awry of the unique constraint. --- bookwyrm/migrations/0037_auto_20210118_1954.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/bookwyrm/migrations/0037_auto_20210118_1954.py b/bookwyrm/migrations/0037_auto_20210118_1954.py index 9da0265d..97ba8808 100644 --- a/bookwyrm/migrations/0037_auto_20210118_1954.py +++ b/bookwyrm/migrations/0037_auto_20210118_1954.py @@ -2,6 +2,15 @@ from django.db import migrations, models +def empty_to_null(apps, schema_editor): + User = apps.get_model("bookwyrm", "User") + db_alias = schema_editor.connection.alias + User.objects.using(db_alias).filter(email="").update(email=None) + +def null_to_empty(apps, schema_editor): + User = apps.get_model("bookwyrm", "User") + db_alias = schema_editor.connection.alias + User.objects.using(db_alias).filter(email=None).update(email="") class Migration(migrations.Migration): @@ -14,6 +23,12 @@ class Migration(migrations.Migration): name='shelfbook', options={'ordering': ('-created_date',)}, ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(max_length=254, null=True), + ), + migrations.RunPython(empty_to_null, null_to_empty), migrations.AlterField( model_name='user', name='email', From d994d8d3c82f7d019a4714bdb2a8591b55f30529 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 24 Jan 2021 17:07:19 -0800 Subject: [PATCH 008/280] Moves blocking side effects to model --- bookwyrm/incoming.py | 11 +---------- bookwyrm/models/relationship.py | 20 ++++++++++++++++++++ bookwyrm/templates/snippets/user_header.html | 4 ++++ bookwyrm/urls.py | 2 ++ bookwyrm/views/__init__.py | 1 + 5 files changed, 28 insertions(+), 10 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 5d93756f..3581ed87 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -186,16 +186,7 @@ def handle_block(activity): ''' blocking a user ''' # create "block" databse entry block = activitypub.Block(**activity).to_model(models.UserBlocks) - - # remove follow relationships - models.UserFollows.objects.filter( - Q(user_subject=block.user_subject, user_object=block.user_object) | \ - Q(user_subject=block.user_object, user_object=block.user_subject) - ).delete() - models.UserFollowRequest.objects.filter( - Q(user_subject=block.user_subject, user_object=block.user_object) | \ - Q(user_subject=block.user_object, user_object=block.user_subject) - ).delete() + # the removing relationships is handled in post-save hook in model @app.task diff --git a/bookwyrm/models/relationship.py b/bookwyrm/models/relationship.py index 9ea75a8f..ec84d44f 100644 --- a/bookwyrm/models/relationship.py +++ b/bookwyrm/models/relationship.py @@ -1,5 +1,7 @@ ''' defines relationships between users ''' from django.db import models +from django.db.models import Q +from django.dispatch import receiver from bookwyrm import activitypub from .base_model import ActivitypubMixin, BookWyrmModel @@ -96,3 +98,21 @@ class UserBlocks(UserRelationship): ''' prevent another user from following you and seeing your posts ''' status = 'blocks' activity_serializer = activitypub.Block + + +@receiver(models.signals.post_save, sender=UserBlocks) +#pylint: disable=unused-argument +def execute_after_save(sender, instance, created, *args, **kwargs): + ''' remove follow or follow request rels after a block is created ''' + UserFollows.objects.filter( + Q(user_subject=instance.user_subject, + user_object=instance.user_object) | \ + Q(user_subject=instance.user_object, + user_object=instance.user_subject) + ).delete() + UserFollowRequest.objects.filter( + Q(user_subject=instance.user_subject, + user_object=instance.user_object) | \ + Q(user_subject=instance.user_object, + user_object=instance.user_subject) + ).delete() diff --git a/bookwyrm/templates/snippets/user_header.html b/bookwyrm/templates/snippets/user_header.html index a528bb1c..14216d4b 100644 --- a/bookwyrm/templates/snippets/user_header.html +++ b/bookwyrm/templates/snippets/user_header.html @@ -36,6 +36,10 @@
    {% if not is_self %} {% include 'snippets/follow_button.html' with user=user %} +
    + {% csrf_token %} + +
    {% endif %} {% if is_self and user.follower_requests.all %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 4da0c0c1..bfd57d0a 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -136,4 +136,6 @@ urlpatterns = [ re_path(r'^unfollow/?$', views.unfollow), re_path(r'^accept-follow-request/?$', views.accept_follow_request), re_path(r'^delete-follow-request/?$', views.delete_follow_request), + + re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index b9c26388..1521b268 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -1,6 +1,7 @@ ''' make sure all our nice views are available ''' from .authentication import Login, Register, Logout from .author import Author, EditAuthor +from .block import Block from .books import Book, EditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .direct_message import DirectMessage From ac2ab2981f15955d8d3a2d07a5e33ef6eb25d01c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 25 Jan 2021 14:03:18 -0800 Subject: [PATCH 009/280] ui path to iniate blocks --- bookwyrm/templates/snippets/block_button.html | 11 +++++++ .../templates/snippets/status_options.html | 11 ++----- bookwyrm/templates/snippets/user_header.html | 13 +++++---- bookwyrm/templates/snippets/user_options.html | 14 +++++++++ bookwyrm/views/block.py | 29 +++++++++++++++++++ bookwyrm/views/helpers.py | 7 +++-- 6 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 bookwyrm/templates/snippets/block_button.html create mode 100644 bookwyrm/templates/snippets/user_options.html create mode 100644 bookwyrm/views/block.py diff --git a/bookwyrm/templates/snippets/block_button.html b/bookwyrm/templates/snippets/block_button.html new file mode 100644 index 00000000..ed9bb551 --- /dev/null +++ b/bookwyrm/templates/snippets/block_button.html @@ -0,0 +1,11 @@ +{% if not user in request.user.blocks.all %} +
    + {% csrf_token %} + +
    +{% else %} +
    + {% csrf_token %} + +
    +{% endif %} diff --git a/bookwyrm/templates/snippets/status_options.html b/bookwyrm/templates/snippets/status_options.html index 9b312c7c..2e2e5d35 100644 --- a/bookwyrm/templates/snippets/status_options.html +++ b/bookwyrm/templates/snippets/status_options.html @@ -17,14 +17,9 @@ -
  • -{% endif %} +{% else %}
  • - + {% include 'snippets/block_button.html' with user=status.user %}
  • +{% endif %} {% endblock %} diff --git a/bookwyrm/templates/snippets/user_header.html b/bookwyrm/templates/snippets/user_header.html index 14216d4b..8f5e264a 100644 --- a/bookwyrm/templates/snippets/user_header.html +++ b/bookwyrm/templates/snippets/user_header.html @@ -35,11 +35,14 @@ {% if not is_self %} - {% include 'snippets/follow_button.html' with user=user %} -
    - {% csrf_token %} - -
    +
    +
    + {% include 'snippets/follow_button.html' with user=user %} +
    +
    + {% include 'snippets/user_options.html' with user=user class="is-small" %} +
    +
    {% endif %} {% if is_self and user.follower_requests.all %} diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html new file mode 100644 index 00000000..9515d912 --- /dev/null +++ b/bookwyrm/templates/snippets/user_options.html @@ -0,0 +1,14 @@ +{% extends 'snippets/components/dropdown.html' %} +{% load bookwyrm_tags %} + +{% block dropdown-trigger %} + + More options + +{% endblock %} + +{% block dropdown-list %} +
  • + {% include 'snippets/block_button.html' with user=user %} +
  • +{% endblock %} diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py new file mode 100644 index 00000000..36f64f73 --- /dev/null +++ b/bookwyrm/views/block.py @@ -0,0 +1,29 @@ +''' views for actions you can take in the application ''' +from django.contrib.auth.decorators import login_required +from django.shortcuts import get_object_or_404, redirect +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import models +from bookwyrm.broadcast import broadcast + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +class Block(View): + ''' blocking users ''' + def get(self, request): + ''' list of blocked users? ''' + + def post(self, request, user_id): + ''' block a user ''' + to_block = get_object_or_404(models.User, id=user_id) + block = models.UserBlocks.objects.create( + user_subject=request.user, user_object=to_block) + if not to_block.local: + broadcast( + request.user, + block.to_activity(), + privacy='direct', + direct_recipients=[to_block] + ) + return redirect('/blocks') diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index b0f867b7..5872b2de 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -72,9 +72,10 @@ def get_activity_feed( queryset = queryset.exclude(deleted=True).order_by('-published_date') # exclude blocks from both directions - blocked = models.User.objects.filter(id__in=user.blocks.all()).all() - queryset = queryset.exclude( - Q(user__in=blocked) | Q(user__blocks=user)) + if not user.is_anonymous: + blocked = models.User.objects.filter(id__in=user.blocks.all()).all() + queryset = queryset.exclude( + Q(user__in=blocked) | Q(user__blocks=user)) # you can't see followers only or direct messages if you're not logged in if user.is_anonymous: From 2a6a000e0524af701404e9eaa2be232c55389ddf Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 08:03:16 -0800 Subject: [PATCH 010/280] Moves avatar crop into function and adds test --- bookwyrm/static/images/med.jpg | Bin 10937 -> 0 bytes bookwyrm/static/images/profile.jpg | Bin 34916 -> 0 bytes bookwyrm/static/images/small.jpg | Bin 1122 -> 0 bytes bookwyrm/tests/views/test_user.py | 16 +++++++++++ bookwyrm/views/user.py | 43 ++++++++++++++++------------- 5 files changed, 40 insertions(+), 19 deletions(-) delete mode 100644 bookwyrm/static/images/med.jpg delete mode 100644 bookwyrm/static/images/profile.jpg delete mode 100644 bookwyrm/static/images/small.jpg diff --git a/bookwyrm/static/images/med.jpg b/bookwyrm/static/images/med.jpg deleted file mode 100644 index c275cd1c85dce6988f39a34f6fb2ad1a4aba7662..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10937 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 XS;q9@zlzB({!JnIp8_-Ra_+wYDD(on diff --git a/bookwyrm/static/images/profile.jpg b/bookwyrm/static/images/profile.jpg deleted file mode 100644 index f150ceabebf5e183818405f854f6de336d063068..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 34916 zcmbrlWpEua(=K?-%*@Qp%*@Qp_Az5(j+vR6nHge;nPbKr$IR^5j@j$H@9zDow(kA0 zwLNvDr_|~msi&q#YW4Y8``8Ac%1g^h1Hixl0I<&s@UaVCC?_FdqN=VUEvG2;9|m}$ zgN36zBr5>m=;Yz1E+bB=tEW#2vj;!`zyQDj_yArr3wIYWRaHg6|B)V6{%HpQ7McF( z`agF2-!l*_t=ug>2MGJg=CyEf^Z3M~pIG^ehs!^l_=zzsY|Sh`ap@;!bo(6OC!YJ) zZvJ1q{}0>z7eD^PZkp;60021RCnmM|FJ}CQ&Hs!4&4tv$*3I#=kHaUXbae3i9K(O; z-%Jp#oU}APYr_Bh+yUwU8Gtx|6krbU1lR!_03HCQ&)VtJX8(IR_W#H!1Dro)EIvyY zfH%PHQ^E#d`zg!#+3E>!2Uva9)}QTGpO?dDk5Bq%`+qe6@IN+nx90e#?{mtC@&EwT z-p9uudH?_>2LO1B`1p7!{`hz+0RSM@0DvE<|0D05`f1L~&-TRsX`{>o08l~zfcC!s zX)`Ya06IQxjQiWg%+2gS_ks8+$D9!)?S=j&p_2>Oi3_pGWBmhtlkdTlNP@e@96cjWJA}q`&p&%f@A)=w6qobjq zp5wt`vyFazw*)yz=W_;Fs zaFFmNyuEydr5o3?mM>|tWO7-%Fud~{!`JzrV{zlZ2I&TR`DQT7RnrQ;7iKGXYvi)b z>Pq@+TiVa$7)D==F+1m!<;jRs#`)JGm1}Ir6jTmS`wQo-3+dn#H=+#~M3mm02ioPy zVV2xUVBg^f$B4nsuIAB~#puGOE!syMR(zpW4bAkCJHFi3_&p~2Q>Iz2NjV)FU!i{C zN5jHoCrumWrDFIE^)D%CXWMnX!wZm_XQA5F&2!IL?C|9(VgZ}i5ZT5zUjSVP2^doK zHY_!Gv-8#KJv-U)6KDycKzGdp-sKMHzlT$^<`8PkR63^~TV!dRVE0kE8fP~@NmGtk3N z6v&Nwh@rEpU$`OTG*DTCa7%cVD2WH|h9w z_qLUKg6oD^8@J)zH6g$@Z7U{O*|yxRzXcI#-u#tpCXXLgZ0b$64P}Owy^)poF72=1 zNWPOAxt(*6h;1^puc2I$(2a7W?v=6TW=-223ZZA|!j73sc2i!8{k?@ch}`V*@&3bcD5AQd xZ=@u}2V$>Ihm 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 diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 0e2ad904..6a51aeda 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -1,5 +1,9 @@ ''' test for app action functionality ''' +import pathlib from unittest.mock import patch +from PIL import Image + +from django.core.files.base import ContentFile from django.template.response import TemplateResponse from django.test import TestCase from django.test.client import RequestFactory @@ -97,3 +101,15 @@ class UserViews(TestCase): with patch('bookwyrm.broadcast.broadcast_task.delay'): view(request) self.assertEqual(self.local_user.name, 'New Name') + + + def test_crop_avatar(self): + ''' reduce that image size ''' + image_file = pathlib.Path(__file__).parent.joinpath( + '../../static/images/no_cover.jpg') + image = Image.open(image_file) + + result = views.user.crop_avatar(image) + self.assertIsInstance(result, ContentFile) + image_result = Image.open(result) + self.assertEqual(image_result.size, (120, 120)) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 6f7873d5..b65fb48f 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -159,30 +159,35 @@ class EditUser(View): if 'avatar' in form.files: # crop and resize avatar upload image = Image.open(form.files['avatar']) - target_size = 120 - width, height = image.size - thumbnail_scale = height / (width / target_size) if height > width \ - else width / (height / target_size) - image.thumbnail([thumbnail_scale, thumbnail_scale]) - width, height = image.size - - width_diff = width - target_size - height_diff = height - target_size - cropped = image.crop(( - int(width_diff / 2), - int(height_diff / 2), - int(width - (width_diff / 2)), - int(height - (height_diff / 2)) - )) - output = BytesIO() - cropped.save(output, format=image.format) - ContentFile(output.getvalue()) + image = crop_avatar(image) # set the name to a hash extension = form.files['avatar'].name.split('.')[-1] filename = '%s.%s' % (uuid4(), extension) - user.avatar.save(filename, ContentFile(output.getvalue())) + user.avatar.save(filename, image) user.save() broadcast(user, user.to_update_activity(user)) return redirect(user.local_path) + + +def crop_avatar(image): + ''' reduce the size and make an avatar square ''' + target_size = 120 + width, height = image.size + thumbnail_scale = height / (width / target_size) if height > width \ + else width / (height / target_size) + image.thumbnail([thumbnail_scale, thumbnail_scale]) + width, height = image.size + + width_diff = width - target_size + height_diff = height - target_size + cropped = image.crop(( + int(width_diff / 2), + int(height_diff / 2), + int(width - (width_diff / 2)), + int(height - (height_diff / 2)) + )) + output = BytesIO() + cropped.save(output, format=image.format) + return ContentFile(output.getvalue()) From 681f5482fdf50dbb1d05b8a37b4f9649fd4e4ccc Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 08:07:38 -0800 Subject: [PATCH 011/280] Don't allow blocked users to access user page --- bookwyrm/views/user.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 6f7873d5..2a4211b8 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -31,6 +31,11 @@ class User(View): except models.User.DoesNotExist: return HttpResponseNotFound() + # make sure we're not blocked + if request.user.is_authenticated: + if request.user in user.blocks.all(): + return HttpResponseNotFound() + if is_api_request(request): # we have a json request return ActivitypubResponse(user.to_activity()) From 3f011445e29b63dd1aaf40c1455096caf80b99e1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 08:31:55 -0800 Subject: [PATCH 012/280] Hide user pages to blocked users --- bookwyrm/tests/views/test_user.py | 39 +++++++++++++++++++++++++++++++ bookwyrm/views/helpers.py | 6 +++++ bookwyrm/views/user.py | 15 ++++++++---- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 0e2ad904..95c4db0a 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -16,6 +16,9 @@ class UserViews(TestCase): self.local_user = models.User.objects.create_user( 'mouse@local.com', 'mouse@mouse.mouse', 'password', local=True, localname='mouse') + self.rat = models.User.objects.create_user( + 'rat@local.com', 'rat@rat.rat', 'password', + local=True, localname='rat') def test_user_page(self): @@ -37,6 +40,18 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) + def test_user_page_blocked(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.User.as_view() + request = self.factory.get('') + request.user = self.local_user + self.rat.blocks.add(self.local_user) + with patch('bookwyrm.views.user.is_api_request') as is_api: + is_api.return_value = False + result = view(request, 'rat') + self.assertEqual(result.status_code, 404) + + def test_followers_page(self): ''' there are so many views, this just makes sure it LOADS ''' view = views.Followers.as_view() @@ -56,6 +71,18 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) + def test_followers_page_blocked(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Followers.as_view() + request = self.factory.get('') + request.user = self.local_user + self.rat.blocks.add(self.local_user) + with patch('bookwyrm.views.user.is_api_request') as is_api: + is_api.return_value = False + result = view(request, 'rat') + self.assertEqual(result.status_code, 404) + + def test_following_page(self): ''' there are so many views, this just makes sure it LOADS ''' view = views.Following.as_view() @@ -75,6 +102,18 @@ class UserViews(TestCase): self.assertEqual(result.status_code, 200) + def test_following_page_blocked(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Following.as_view() + request = self.factory.get('') + request.user = self.local_user + self.rat.blocks.add(self.local_user) + with patch('bookwyrm.views.user.is_api_request') as is_api: + is_api.return_value = False + result = view(request, 'rat') + self.assertEqual(result.status_code, 404) + + def test_edit_profile_page(self): ''' there are so many views, this just makes sure it LOADS ''' view = views.EditUser.as_view() diff --git a/bookwyrm/views/helpers.py b/bookwyrm/views/helpers.py index 5872b2de..6bda81c8 100644 --- a/bookwyrm/views/helpers.py +++ b/bookwyrm/views/helpers.py @@ -190,3 +190,9 @@ def handle_reading_status(user, shelf, book, privacy): status.save() broadcast(user, status.to_create_activity(user)) + +def is_blocked(viewer, user): + ''' is this viewer blocked by the user? ''' + if viewer.is_authenticated and viewer in user.blocks.all(): + return True + return False diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 2a4211b8..acf19c44 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -18,7 +18,7 @@ from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.broadcast import broadcast from bookwyrm.settings import PAGE_LENGTH from .helpers import get_activity_feed, get_user_from_username, is_api_request -from .helpers import object_visible_to_user +from .helpers import is_blocked, object_visible_to_user # pylint: disable= no-self-use @@ -32,9 +32,8 @@ class User(View): return HttpResponseNotFound() # make sure we're not blocked - if request.user.is_authenticated: - if request.user in user.blocks.all(): - return HttpResponseNotFound() + if is_blocked(request.user, user): + return HttpResponseNotFound() if is_api_request(request): # we have a json request @@ -102,6 +101,10 @@ class Followers(View): except models.User.DoesNotExist: return HttpResponseNotFound() + # make sure we're not blocked + if is_blocked(request.user, user): + return HttpResponseNotFound() + if is_api_request(request): return ActivitypubResponse( user.to_followers_activity(**request.GET)) @@ -123,6 +126,10 @@ class Following(View): except models.User.DoesNotExist: return HttpResponseNotFound() + # make sure we're not blocked + if is_blocked(request.user, user): + return HttpResponseNotFound() + if is_api_request(request): return ActivitypubResponse( user.to_following_activity(**request.GET)) From acfc865d4ef1b7d65e5bb2f8b36f7c1056e22055 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 09:56:01 -0800 Subject: [PATCH 013/280] Adds blocked users view also refactors the setting view --- bookwyrm/templates/blocks.html | 21 +++ bookwyrm/templates/change_password.html | 19 +++ bookwyrm/templates/edit_user.html | 112 ++++++--------- bookwyrm/templates/preferences_layout.html | 31 ++++ bookwyrm/templates/snippets/block_button.html | 4 +- bookwyrm/tests/views/test_authentication.py | 103 ------------- bookwyrm/tests/views/test_password.py | 136 ++++++++++++++++++ bookwyrm/urls.py | 3 +- bookwyrm/views/block.py | 5 +- bookwyrm/views/password.py | 8 ++ bookwyrm/views/user.py | 9 +- 11 files changed, 273 insertions(+), 178 deletions(-) create mode 100644 bookwyrm/templates/blocks.html create mode 100644 bookwyrm/templates/change_password.html create mode 100644 bookwyrm/templates/preferences_layout.html create mode 100644 bookwyrm/tests/views/test_password.py diff --git a/bookwyrm/templates/blocks.html b/bookwyrm/templates/blocks.html new file mode 100644 index 00000000..0e725e4b --- /dev/null +++ b/bookwyrm/templates/blocks.html @@ -0,0 +1,21 @@ +{% extends 'preferences_layout.html' %} + +{% block header %} +Blocked Users +{% endblock %} + +{% block panel %} +

    +{% endblock %} + diff --git a/bookwyrm/templates/change_password.html b/bookwyrm/templates/change_password.html new file mode 100644 index 00000000..c373dfc8 --- /dev/null +++ b/bookwyrm/templates/change_password.html @@ -0,0 +1,19 @@ +{% extends 'preferences_layout.html' %} +{% block header %} +Change Password +{% endblock %} + +{% block panel %} +
    + {% csrf_token %} +
    + + +
    +
    + + +
    + +
    +{% endblock %} diff --git a/bookwyrm/templates/edit_user.html b/bookwyrm/templates/edit_user.html index 413f2cae..ee9ddb22 100644 --- a/bookwyrm/templates/edit_user.html +++ b/bookwyrm/templates/edit_user.html @@ -1,66 +1,48 @@ -{% extends 'layout.html' %} -{% block content %} -
    -
    -

    Profile

    - {% if form.non_field_errors %} -

    {{ form.non_field_errors }}

    - {% endif %} -
    - {% csrf_token %} -
    - - {{ form.avatar }} - {% for error in form.avatar.errors %} -

    {{ error | escape }}

    - {% endfor %} -
    -
    - - {{ form.name }} - {% for error in form.name.errors %} -

    {{ error | escape }}

    - {% endfor %} -
    -
    - - {{ form.summary }} - {% for error in form.summary.errors %} -

    {{ error | escape }}

    - {% endfor %} -
    -
    - - {{ form.email }} - {% for error in form.email.errors %} -

    {{ error | escape }}

    - {% endfor %} -
    -
    - -
    - -
    -
    -
    -
    -

    Change password

    -
    - {% csrf_token %} -
    - - -
    -
    - - -
    - -
    -
    -
    -
    +{% extends 'preferences_layout.html' %} +{% block header %} +Edit Profile +{% endblock %} + +{% block panel %} +{% if form.non_field_errors %} +

    {{ form.non_field_errors }}

    +{% endif %} +
    + {% csrf_token %} +
    + + {{ form.avatar }} + {% for error in form.avatar.errors %} +

    {{ error | escape }}

    + {% endfor %} +
    +
    + + {{ form.name }} + {% for error in form.name.errors %} +

    {{ error | escape }}

    + {% endfor %} +
    +
    + + {{ form.summary }} + {% for error in form.summary.errors %} +

    {{ error | escape }}

    + {% endfor %} +
    +
    + + {{ form.email }} + {% for error in form.email.errors %} +

    {{ error | escape }}

    + {% endfor %} +
    +
    + +
    + +
    {% endblock %} diff --git a/bookwyrm/templates/preferences_layout.html b/bookwyrm/templates/preferences_layout.html new file mode 100644 index 00000000..de2fe0df --- /dev/null +++ b/bookwyrm/templates/preferences_layout.html @@ -0,0 +1,31 @@ +{% extends 'layout.html' %} +{% block content %} + +
    +

    {% block header %}{% endblock %}

    +
    + +
    + +
    + {% block panel %}{% endblock %} +
    +
    + +{% endblock %} diff --git a/bookwyrm/templates/snippets/block_button.html b/bookwyrm/templates/snippets/block_button.html index ed9bb551..9e49254d 100644 --- a/bookwyrm/templates/snippets/block_button.html +++ b/bookwyrm/templates/snippets/block_button.html @@ -1,11 +1,11 @@ {% if not user in request.user.blocks.all %}
    {% csrf_token %} - +
    {% else %}
    {% csrf_token %} - +
    {% endif %} diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_authentication.py index b0d09983..65577208 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_authentication.py @@ -42,83 +42,6 @@ class AuthenticationViews(TestCase): self.assertEqual(result.status_code, 302) - def test_password_reset_request(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.PasswordResetRequest.as_view() - request = self.factory.get('') - request.user = self.local_user - - result = view(request) - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'password_reset_request.html') - self.assertEqual(result.status_code, 200) - - - def test_password_reset_request_post(self): - ''' send 'em an email ''' - request = self.factory.post('', {'email': 'aa@bb.ccc'}) - view = views.PasswordResetRequest.as_view() - resp = view(request) - self.assertEqual(resp.status_code, 302) - - request = self.factory.post('', {'email': 'mouse@mouse.com'}) - with patch('bookwyrm.emailing.send_email.delay'): - resp = view(request) - self.assertEqual(resp.template_name, 'password_reset_request.html') - - self.assertEqual( - models.PasswordReset.objects.get().user, self.local_user) - - def test_password_reset(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.PasswordReset.as_view() - code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.get('') - request.user = self.anonymous_user - result = view(request, code.code) - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'password_reset.html') - self.assertEqual(result.status_code, 200) - - - def test_password_reset_post(self): - ''' reset from code ''' - view = views.PasswordReset.as_view() - code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) - with patch('bookwyrm.views.password.login'): - resp = view(request, code.code) - self.assertEqual(resp.status_code, 302) - self.assertFalse(models.PasswordReset.objects.exists()) - - def test_password_reset_wrong_code(self): - ''' reset from code ''' - view = views.PasswordReset.as_view() - models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) - resp = view(request, 'jhgdkfjgdf') - self.assertEqual(resp.template_name, 'password_reset.html') - self.assertTrue(models.PasswordReset.objects.exists()) - - def test_password_reset_mismatch(self): - ''' reset from code ''' - view = views.PasswordReset.as_view() - code = models.PasswordReset.objects.create(user=self.local_user) - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hihi' - }) - resp = view(request, code.code) - self.assertEqual(resp.template_name, 'password_reset.html') - self.assertTrue(models.PasswordReset.objects.exists()) - - def test_register(self): ''' create a user ''' view = views.Register.as_view() @@ -274,29 +197,3 @@ class AuthenticationViews(TestCase): with self.assertRaises(Http404): response = view(request) self.assertEqual(models.User.objects.count(), 2) - - - def test_password_change(self): - ''' change password ''' - view = views.ChangePassword.as_view() - password_hash = self.local_user.password - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hi' - }) - request.user = self.local_user - with patch('bookwyrm.views.password.login'): - view(request) - self.assertNotEqual(self.local_user.password, password_hash) - - def test_password_change_mismatch(self): - ''' change password ''' - view = views.ChangePassword.as_view() - password_hash = self.local_user.password - request = self.factory.post('', { - 'password': 'hi', - 'confirm-password': 'hihi' - }) - request.user = self.local_user - view(request) - self.assertEqual(self.local_user.password, password_hash) diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py new file mode 100644 index 00000000..0f9c8988 --- /dev/null +++ b/bookwyrm/tests/views/test_password.py @@ -0,0 +1,136 @@ +''' test for app action functionality ''' +from unittest.mock import patch + +from django.contrib.auth.models import AnonymousUser +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views + + +class PasswordViews(TestCase): + ''' view user and edit profile ''' + def setUp(self): + ''' we need basic test data and mocks ''' + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + 'mouse@local.com', 'mouse@mouse.com', 'password', + local=True, localname='mouse') + self.anonymous_user = AnonymousUser + self.anonymous_user.is_authenticated = False + + + def test_password_reset_request(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.PasswordResetRequest.as_view() + request = self.factory.get('') + request.user = self.local_user + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'password_reset_request.html') + self.assertEqual(result.status_code, 200) + + + def test_password_reset_request_post(self): + ''' send 'em an email ''' + request = self.factory.post('', {'email': 'aa@bb.ccc'}) + view = views.PasswordResetRequest.as_view() + resp = view(request) + self.assertEqual(resp.status_code, 302) + + request = self.factory.post('', {'email': 'mouse@mouse.com'}) + with patch('bookwyrm.emailing.send_email.delay'): + resp = view(request) + self.assertEqual(resp.template_name, 'password_reset_request.html') + + self.assertEqual( + models.PasswordReset.objects.get().user, self.local_user) + + def test_password_reset(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.PasswordReset.as_view() + code = models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.get('') + request.user = self.anonymous_user + result = view(request, code.code) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'password_reset.html') + self.assertEqual(result.status_code, 200) + + + def test_password_reset_post(self): + ''' reset from code ''' + view = views.PasswordReset.as_view() + code = models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'password': 'hi', + 'confirm-password': 'hi' + }) + with patch('bookwyrm.views.password.login'): + resp = view(request, code.code) + self.assertEqual(resp.status_code, 302) + self.assertFalse(models.PasswordReset.objects.exists()) + + def test_password_reset_wrong_code(self): + ''' reset from code ''' + view = views.PasswordReset.as_view() + models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'password': 'hi', + 'confirm-password': 'hi' + }) + resp = view(request, 'jhgdkfjgdf') + self.assertEqual(resp.template_name, 'password_reset.html') + self.assertTrue(models.PasswordReset.objects.exists()) + + def test_password_reset_mismatch(self): + ''' reset from code ''' + view = views.PasswordReset.as_view() + code = models.PasswordReset.objects.create(user=self.local_user) + request = self.factory.post('', { + 'password': 'hi', + 'confirm-password': 'hihi' + }) + resp = view(request, code.code) + self.assertEqual(resp.template_name, 'password_reset.html') + self.assertTrue(models.PasswordReset.objects.exists()) + + + def test_password_change_get(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.ChangePassword.as_view() + request = self.factory.get('') + request.user = self.local_user + + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'change_password.html') + self.assertEqual(result.status_code, 200) + + + def test_password_change(self): + ''' change password ''' + view = views.ChangePassword.as_view() + password_hash = self.local_user.password + request = self.factory.post('', { + 'password': 'hi', + 'confirm-password': 'hi' + }) + request.user = self.local_user + with patch('bookwyrm.views.password.login'): + view(request) + self.assertNotEqual(self.local_user.password, password_hash) + + def test_password_change_mismatch(self): + ''' change password ''' + view = views.ChangePassword.as_view() + password_hash = self.local_user.password + request = self.factory.post('', { + 'password': 'hi', + 'confirm-password': 'hihi' + }) + request.user = self.local_user + view(request) + self.assertEqual(self.local_user.password, password_hash) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index bfd57d0a..0569fd9c 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -47,7 +47,7 @@ urlpatterns = [ re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()), re_path(r'^password-reset/(?P[A-Za-z0-9]+)/?$', views.PasswordReset.as_view()), - re_path(r'^change-password/?$', views.ChangePassword), + re_path(r'^change-password/?$', views.ChangePassword.as_view()), # invites re_path(r'^invite/?$', views.ManageInvites.as_view()), @@ -137,5 +137,6 @@ urlpatterns = [ re_path(r'^accept-follow-request/?$', views.accept_follow_request), re_path(r'^delete-follow-request/?$', views.delete_follow_request), + re_path(r'^block/?$', views.Block.as_view()), re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index 36f64f73..6158f373 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -1,6 +1,7 @@ ''' views for actions you can take in the application ''' from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect +from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View @@ -13,6 +14,8 @@ class Block(View): ''' blocking users ''' def get(self, request): ''' list of blocked users? ''' + return TemplateResponse( + request, 'blocks.html', {'title': 'Blocked Users'}) def post(self, request, user_id): ''' block a user ''' @@ -26,4 +29,4 @@ class Block(View): privacy='direct', direct_recipients=[to_block] ) - return redirect('/blocks') + return redirect('/block') diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index 915659e3..06ddc1da 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -88,6 +88,14 @@ class PasswordReset(View): @method_decorator(login_required, name='dispatch') class ChangePassword(View): ''' change password as logged in user ''' + def get(self, request): + ''' change password page ''' + data = { + 'title': 'Change Password', + 'user': request.user, + } + return TemplateResponse(request, 'change_password.html', data) + def post(self, request): ''' allow a user to change their password ''' new_password = request.POST.get('password') diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index acf19c44..25515bb1 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -147,14 +147,11 @@ class Following(View): class EditUser(View): ''' edit user view ''' def get(self, request): - ''' profile page for a user ''' - user = request.user - - form = forms.EditUserForm(instance=request.user) + ''' edit profile page for a user ''' data = { 'title': 'Edit profile', - 'form': form, - 'user': user, + 'form': forms.EditUserForm(instance=request.user), + 'user': request.user, } return TemplateResponse(request, 'edit_user.html', data) From 22e4138555317bbcd16126c13795f6cd45a15a12 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 13:00:36 -0800 Subject: [PATCH 014/280] unblock --- bookwyrm/incoming.py | 18 +++++++++++-- .../templates/snippets/status_options.html | 2 +- bookwyrm/templates/snippets/user_options.html | 2 +- bookwyrm/tests/test_incoming.py | 17 ++++++++++++ bookwyrm/urls.py | 1 + bookwyrm/views/__init__.py | 2 +- bookwyrm/views/block.py | 26 +++++++++++++++++++ 7 files changed, 63 insertions(+), 5 deletions(-) diff --git a/bookwyrm/incoming.py b/bookwyrm/incoming.py index 3581ed87..1e42d32a 100644 --- a/bookwyrm/incoming.py +++ b/bookwyrm/incoming.py @@ -3,7 +3,6 @@ import json from urllib.parse import urldefrag import django.db.utils -from django.db.models import Q from django.http import HttpResponse from django.http import HttpResponseBadRequest, HttpResponseNotFound from django.views.decorators.csrf import csrf_exempt @@ -64,6 +63,7 @@ def shared_inbox(request): 'Follow': handle_unfollow, 'Like': handle_unfavorite, 'Announce': handle_unboost, + 'Block': handle_unblock, }, 'Update': { 'Person': handle_update_user, @@ -185,10 +185,24 @@ def handle_follow_reject(activity): def handle_block(activity): ''' blocking a user ''' # create "block" databse entry - block = activitypub.Block(**activity).to_model(models.UserBlocks) + activitypub.Block(**activity).to_model(models.UserBlocks) # the removing relationships is handled in post-save hook in model +@app.task +def handle_unblock(activity): + ''' undoing a block ''' + try: + block_id = activity['object']['id'] + except KeyError: + return + try: + block = models.UserBlocks.objects.get(remote_id=block_id) + except models.UserBlocks.DoesNotExist: + return + block.delete() + + @app.task def handle_create(activity): ''' someone did something, good on them ''' diff --git a/bookwyrm/templates/snippets/status_options.html b/bookwyrm/templates/snippets/status_options.html index 2e2e5d35..b5887b1d 100644 --- a/bookwyrm/templates/snippets/status_options.html +++ b/bookwyrm/templates/snippets/status_options.html @@ -19,7 +19,7 @@ {% else %}
  • - {% include 'snippets/block_button.html' with user=status.user %} + {% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
  • {% endif %} {% endblock %} diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html index 9515d912..2c163034 100644 --- a/bookwyrm/templates/snippets/user_options.html +++ b/bookwyrm/templates/snippets/user_options.html @@ -9,6 +9,6 @@ {% block dropdown-list %}
  • - {% include 'snippets/block_button.html' with user=user %} + {% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
  • {% endblock %} diff --git a/bookwyrm/tests/test_incoming.py b/bookwyrm/tests/test_incoming.py index 024c8e25..1ee7c59e 100644 --- a/bookwyrm/tests/test_incoming.py +++ b/bookwyrm/tests/test_incoming.py @@ -566,3 +566,20 @@ class Incoming(TestCase): self.assertFalse(models.UserFollows.objects.exists()) self.assertFalse(models.UserFollowRequest.objects.exists()) + + def test_handle_unblock(self): + ''' undoing a block ''' + activity = { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://friend.camp/users/tripofmice#blocks/1155/undo", + "type": "Undo", + "actor": "https://friend.camp/users/tripofmice", + "object": { + "id": "https://friend.camp/0a7d85f7-6359-4c03-8ab6-74e61a8fb678", + "type": "Block", + "actor": "https://friend.camp/users/tripofmice", + "object": "https://1b1a78582461.ngrok.io/user/mouse" + } + } + + self.remote_user.blocks.add(self.local_user) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 0569fd9c..4f9a43ea 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -139,4 +139,5 @@ urlpatterns = [ re_path(r'^block/?$', views.Block.as_view()), re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), + re_path(r'^unblock/(?P\d+)/?$', views.unblock), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 1521b268..e3ac29c8 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -1,7 +1,7 @@ ''' make sure all our nice views are available ''' from .authentication import Login, Register, Logout from .author import Author, EditAuthor -from .block import Block +from .block import Block, unblock from .books import Book, EditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .direct_message import DirectMessage diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index 6158f373..fb95479a 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -1,9 +1,11 @@ ''' views for actions you can take in the application ''' from django.contrib.auth.decorators import login_required +from django.http import HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View +from django.views.decorators.http import require_POST from bookwyrm import models from bookwyrm.broadcast import broadcast @@ -30,3 +32,27 @@ class Block(View): direct_recipients=[to_block] ) return redirect('/block') + + +@require_POST +@login_required +def unblock(request, user_id): + ''' undo a block ''' + to_unblock = get_object_or_404(models.User, id=user_id) + try: + block = models.UserBlocks.objects.get( + user_subject=request.user, + user_object=to_unblock, + ) + except models.UserBlocks.DoesNotExist: + return HttpResponseNotFound() + + if not to_unblock.local: + broadcast( + request.user, + block.to_undo_activity(request.user), + privacy='direct', + direct_recipients=[to_unblock] + ) + block.delete() + return redirect('/block') From 369b24f9ec8da77c8c7c1e0c7f211093c5f564b8 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 13:02:04 -0800 Subject: [PATCH 015/280] null state for block page --- bookwyrm/templates/blocks.html | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/blocks.html b/bookwyrm/templates/blocks.html index 0e725e4b..1df49816 100644 --- a/bookwyrm/templates/blocks.html +++ b/bookwyrm/templates/blocks.html @@ -5,6 +5,9 @@ Blocked Users {% endblock %} {% block panel %} +{% if not request.user.blocks.exists %} +

    No users currently blocked.

    +{% else %}
      {% for user in request.user.blocks.all %}
    • @@ -15,7 +18,7 @@ Blocked Users {% include 'snippets/block_button.html' with user=user %}

    • -{% endfor %} +{% endfor %}
    +{% endif %} {% endblock %} - From 36486ca731f938ac6de68c804a49e6d4a0090946 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 26 Jan 2021 13:02:14 -0800 Subject: [PATCH 016/280] block/unblock view tests --- bookwyrm/tests/views/test_block.py | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 bookwyrm/tests/views/test_block.py diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py new file mode 100644 index 00000000..fa5254d1 --- /dev/null +++ b/bookwyrm/tests/views/test_block.py @@ -0,0 +1,68 @@ +''' test for app action functionality ''' +from unittest.mock import patch +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models, views + + +class BlockViews(TestCase): + ''' view user and edit profile ''' + def setUp(self): + ''' we need basic test data and mocks ''' + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + 'mouse@local.com', 'mouse@mouse.mouse', 'password', + local=True, localname='mouse') + with patch('bookwyrm.models.user.set_remote_server.delay'): + 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', + ) + + + def test_block_get(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Block.as_view() + request = self.factory.get('') + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'blocks.html') + self.assertEqual(result.status_code, 200) + + def test_block_post(self): + ''' create a "block" database entry from an activity ''' + view = views.Block.as_view() + self.local_user.followers.add(self.remote_user) + models.UserFollowRequest.objects.create( + user_subject=self.local_user, + user_object=self.remote_user) + self.assertTrue(models.UserFollows.objects.exists()) + self.assertTrue(models.UserFollowRequest.objects.exists()) + + request = self.factory.post('') + request.user = self.local_user + with patch('bookwyrm.broadcast.broadcast_task.delay'): + view(request, self.remote_user.id) + block = models.UserBlocks.objects.get() + self.assertEqual(block.user_subject, self.local_user) + self.assertEqual(block.user_object, self.remote_user) + + self.assertFalse(models.UserFollows.objects.exists()) + self.assertFalse(models.UserFollowRequest.objects.exists()) + + def test_unblock(self): + ''' undo a block ''' + self.local_user.blocks.add(self.remote_user) + request = self.factory.post('') + request.user = self.local_user + + with patch('bookwyrm.broadcast.broadcast_task.delay'): + views.block.unblock(request, self.remote_user.id) + + self.assertFalse(models.UserBlocks.objects.exists()) From e2935b6364ff8359d1fc25f7fd2df1602f30109f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 09:27:29 -0800 Subject: [PATCH 017/280] Fixes shelve tests --- bookwyrm/tests/views/test_shelf.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 64966a77..35e07953 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -74,7 +74,7 @@ class ShelfViews(TestCase): 'name': 'To Read', }) request.user = self.local_user - view(request, self.local_user.username, shelf.id) + view(request, self.local_user.username, shelf.identifier) shelf.refresh_from_db() self.assertEqual(shelf.privacy, 'unlisted') @@ -94,7 +94,7 @@ class ShelfViews(TestCase): 'name': 'cool name' }) request.user = self.local_user - view(request, request.user.username, shelf.id) + view(request, request.user.username, shelf.identifier) shelf.refresh_from_db() self.assertEqual(shelf.name, 'cool name') @@ -114,7 +114,7 @@ class ShelfViews(TestCase): 'name': 'cool name' }) request.user = self.local_user - view(request, request.user.username, shelf.id) + view(request, request.user.username, shelf.identifier) self.assertEqual(shelf.name, 'To Read') From 5377a8f1878b22490d8f0cd8e6aed6f6ad4d9a66 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 09:29:34 -0800 Subject: [PATCH 018/280] Fixes reading view --- bookwyrm/views/reading.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/views/reading.py b/bookwyrm/views/reading.py index 565fc179..c2c72509 100644 --- a/bookwyrm/views/reading.py +++ b/bookwyrm/views/reading.py @@ -30,8 +30,8 @@ def start_reading(request, book_id): if readthrough: readthrough.save() - # create a progress update if we have a page - readthrough.create_update() + # create a progress update if we have a page + readthrough.create_update() # shelve the book if request.POST.get('reshelve', True): From 1a54ed875baa5043f288bbe40d09cab69171a972 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 09:30:03 -0800 Subject: [PATCH 019/280] easier way to get user statuses queryset --- bookwyrm/views/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 668ef205..4afda01b 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -73,7 +73,7 @@ class User(View): activities = get_activity_feed( request.user, ['public', 'unlisted', 'followers'], - queryset=models.Status.objects.filter(user=user) + queryset=user.status_set ) paginated = Paginator(activities, PAGE_LENGTH) goal = models.AnnualGoal.objects.filter( From b52aa5bee9bfea772277aa3bc3012ca10abe1562 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 09:31:01 -0800 Subject: [PATCH 020/280] Fixes status tests --- bookwyrm/tests/views/test_status.py | 3 ++- bookwyrm/views/shelf.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index 6e0c73df..50c458ac 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -176,7 +176,8 @@ class StatusViews(TestCase): reply = models.Status.replies(status).first() self.assertEqual(reply.content, '

    right

    ') self.assertEqual(reply.user, user) - self.assertTrue(self.remote_user in reply.mention_users.all()) + # the mentioned user in the parent post is only included if @'ed + self.assertFalse(self.remote_user in reply.mention_users.all()) self.assertTrue(self.local_user in reply.mention_users.all()) def test_find_mentions(self): diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 15b2bd28..ae8a494b 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -65,6 +65,7 @@ class Shelf(View): return TemplateResponse(request, 'shelf.html', data) @method_decorator(login_required, name='dispatch') + # pylint: disable=unused-argument def post(self, request, username, shelf_identifier): ''' edit a shelf ''' try: From c2d0c4b0221fe0c3d3cf1da36bd7e1145c2255e8 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 09:31:11 -0800 Subject: [PATCH 021/280] Adds missing init file for views tests this is important!! --- bookwyrm/tests/views/__init__.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 bookwyrm/tests/views/__init__.py diff --git a/bookwyrm/tests/views/__init__.py b/bookwyrm/tests/views/__init__.py new file mode 100644 index 00000000..b6e690fd --- /dev/null +++ b/bookwyrm/tests/views/__init__.py @@ -0,0 +1 @@ +from . import * From 7d1c5fb0f2b97376a97052f0e706637a59fe7c89 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Wed, 27 Jan 2021 10:48:59 -0800 Subject: [PATCH 022/280] Patches broadcast call in reading test --- bookwyrm/tests/views/test_reading.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/bookwyrm/tests/views/test_reading.py b/bookwyrm/tests/views/test_reading.py index 436dbf68..89716a48 100644 --- a/bookwyrm/tests/views/test_reading.py +++ b/bookwyrm/tests/views/test_reading.py @@ -45,7 +45,8 @@ class ReadingViews(TestCase): 'start_date': '2020-01-05', }) request.user = self.local_user - views.start_reading(request, self.book.id) + with patch('bookwyrm.broadcast.broadcast_task.delay'): + views.start_reading(request, self.book.id) self.assertEqual(shelf.books.get(), self.book) @@ -73,7 +74,8 @@ class ReadingViews(TestCase): request = self.factory.post('') request.user = self.local_user - views.start_reading(request, self.book.id) + with patch('bookwyrm.broadcast.broadcast_task.delay'): + views.start_reading(request, self.book.id) self.assertFalse(to_read_shelf.books.exists()) self.assertEqual(shelf.books.get(), self.book) @@ -95,7 +97,9 @@ class ReadingViews(TestCase): 'id': readthrough.id, }) request.user = self.local_user - views.finish_reading(request, self.book.id) + + with patch('bookwyrm.broadcast.broadcast_task.delay'): + views.finish_reading(request, self.book.id) self.assertEqual(shelf.books.get(), self.book) From 9c2813545c2efe5304aab7a910e9be905017342e Mon Sep 17 00:00:00 2001 From: Chad Nelson Date: Wed, 27 Jan 2021 21:23:20 -0500 Subject: [PATCH 023/280] Closer to some passing tests --- bookwyrm/tests/views/test_rss_feed.py | 7 ++++--- bookwyrm/views/rss_feed.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py index 2e71e96e..bf0fb9a3 100644 --- a/bookwyrm/tests/views/test_rss_feed.py +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -9,7 +9,7 @@ from bookwyrm import models from bookwyrm.views import rss_feed from bookwyrm.settings import DOMAIN -class RssFeed(TestCase): +class RssFeedView(TestCase): ''' rss feed behaves as expected ''' def setUp(self): self.user = models.User.objects.create_user( @@ -37,8 +37,9 @@ class RssFeed(TestCase): def test_rss_feed(self): + view = rss_feed.RssFeed() request = self.factory.get('/user/rss_user/rss') - response = RssFeed(request) - self.assertEqual(response.status_code, 200) + result = view(request, username=self.user.username) + self.assertEqual(result.status_code, 200) self.assertEqual(False, True) diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index ee8063a6..d6bcd174 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -21,7 +21,7 @@ class RssFeed(Feed): def items(self, obj): - return get_activity_feed(obj, ['public', 'unlisted']) + return get_activity_feed(obj, ['public', 'unlisted'], queryset=obj.status_set) def item_link(self, item): From 4274bf0508dcb6cba6464ed6ace9bc3459797d96 Mon Sep 17 00:00:00 2001 From: Chad Nelson Date: Wed, 27 Jan 2021 22:13:35 -0500 Subject: [PATCH 024/280] Patch site settings so this can pass --- bookwyrm/tests/views/test_rss_feed.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/bookwyrm/tests/views/test_rss_feed.py b/bookwyrm/tests/views/test_rss_feed.py index bf0fb9a3..cbc97af7 100644 --- a/bookwyrm/tests/views/test_rss_feed.py +++ b/bookwyrm/tests/views/test_rss_feed.py @@ -1,7 +1,6 @@ ''' testing import ''' from unittest.mock import patch - from django.test import RequestFactory, TestCase import responses @@ -12,6 +11,9 @@ from bookwyrm.settings import DOMAIN class RssFeedView(TestCase): ''' rss feed behaves as expected ''' def setUp(self): + + self.site = models.SiteSettings.objects.create() + self.user = models.User.objects.create_user( 'rss_user', 'rss@test.rss', 'password', local=True) @@ -39,7 +41,12 @@ class RssFeedView(TestCase): def test_rss_feed(self): view = rss_feed.RssFeed() request = self.factory.get('/user/rss_user/rss') - result = view(request, username=self.user.username) + with patch("bookwyrm.models.SiteSettings.objects.get") as site: + site.return_value = self.site + result = view(request, username=self.user.username) self.assertEqual(result.status_code, 200) - self.assertEqual(False, True) + + self.assertIn(b"Status updates from rss_user", result.content) + self.assertIn( b"a sickening sense", result.content) + self.assertIn(b"Example Edition", result.content) From 68d00d590ccd06c32a48d6d0a20ff71ee078121d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 08:00:19 -0800 Subject: [PATCH 025/280] Allows blockquote markdown --- bookwyrm/sanitize_html.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/sanitize_html.py b/bookwyrm/sanitize_html.py index de13ede8..be7fb56f 100644 --- a/bookwyrm/sanitize_html.py +++ b/bookwyrm/sanitize_html.py @@ -7,7 +7,7 @@ class InputHtmlParser(HTMLParser):#pylint: disable=abstract-method def __init__(self): HTMLParser.__init__(self) self.allowed_tags = [ - 'p', 'br', + 'p', 'blockquote', 'br', 'b', 'i', 'strong', 'em', 'pre', 'a', 'span', 'ul', 'ol', 'li' ] From 6ccf7841e1b3afb1aeac5ea232cb5c78fd856493 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 08:51:36 -0800 Subject: [PATCH 026/280] Moves components out of snippets --- bookwyrm/templates/{snippets => }/components/card.html | 0 bookwyrm/templates/{snippets => }/components/dropdown.html | 0 bookwyrm/templates/{snippets => }/components/modal.html | 0 bookwyrm/templates/snippets/delete_readthrough_modal.html | 2 +- bookwyrm/templates/snippets/finish_reading_modal.html | 2 +- bookwyrm/templates/snippets/goal_card.html | 2 +- bookwyrm/templates/snippets/shelf_selector.html | 2 +- bookwyrm/templates/snippets/shelve_button_dropdown.html | 2 +- bookwyrm/templates/snippets/start_reading_modal.html | 2 +- bookwyrm/templates/snippets/status_body.html | 2 +- bookwyrm/templates/snippets/status_options.html | 2 +- bookwyrm/templates/snippets/user_options.html | 2 +- 12 files changed, 9 insertions(+), 9 deletions(-) rename bookwyrm/templates/{snippets => }/components/card.html (100%) rename bookwyrm/templates/{snippets => }/components/dropdown.html (100%) rename bookwyrm/templates/{snippets => }/components/modal.html (100%) diff --git a/bookwyrm/templates/snippets/components/card.html b/bookwyrm/templates/components/card.html similarity index 100% rename from bookwyrm/templates/snippets/components/card.html rename to bookwyrm/templates/components/card.html diff --git a/bookwyrm/templates/snippets/components/dropdown.html b/bookwyrm/templates/components/dropdown.html similarity index 100% rename from bookwyrm/templates/snippets/components/dropdown.html rename to bookwyrm/templates/components/dropdown.html diff --git a/bookwyrm/templates/snippets/components/modal.html b/bookwyrm/templates/components/modal.html similarity index 100% rename from bookwyrm/templates/snippets/components/modal.html rename to bookwyrm/templates/components/modal.html diff --git a/bookwyrm/templates/snippets/delete_readthrough_modal.html b/bookwyrm/templates/snippets/delete_readthrough_modal.html index c04a1d90..19c4b4f0 100644 --- a/bookwyrm/templates/snippets/delete_readthrough_modal.html +++ b/bookwyrm/templates/snippets/delete_readthrough_modal.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/modal.html' %} +{% extends 'components/modal.html' %} {% block modal-title %}Delete these read dates?{% endblock %} {% block modal-body %} {% if readthrough.progress_updates|length > 0 %} diff --git a/bookwyrm/templates/snippets/finish_reading_modal.html b/bookwyrm/templates/snippets/finish_reading_modal.html index 10af1162..c3407631 100644 --- a/bookwyrm/templates/snippets/finish_reading_modal.html +++ b/bookwyrm/templates/snippets/finish_reading_modal.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/modal.html' %} +{% extends 'components/modal.html' %} {% block modal-title %} Finish "{{ book.title }}" diff --git a/bookwyrm/templates/snippets/goal_card.html b/bookwyrm/templates/snippets/goal_card.html index b453d6e4..e26cd5a3 100644 --- a/bookwyrm/templates/snippets/goal_card.html +++ b/bookwyrm/templates/snippets/goal_card.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/card.html' %} +{% extends 'components/card.html' %} {% block card-header %}

    diff --git a/bookwyrm/templates/snippets/shelf_selector.html b/bookwyrm/templates/snippets/shelf_selector.html index f6d5607f..50d7e750 100644 --- a/bookwyrm/templates/snippets/shelf_selector.html +++ b/bookwyrm/templates/snippets/shelf_selector.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/dropdown.html' %} +{% extends 'components/dropdown.html' %} {% block dropdown-trigger %} Change shelf diff --git a/bookwyrm/templates/snippets/shelve_button_dropdown.html b/bookwyrm/templates/snippets/shelve_button_dropdown.html index 80c76d41..50140e3f 100644 --- a/bookwyrm/templates/snippets/shelve_button_dropdown.html +++ b/bookwyrm/templates/snippets/shelve_button_dropdown.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/dropdown.html' %} +{% extends 'components/dropdown.html' %} {% block dropdown-trigger %} More shelves diff --git a/bookwyrm/templates/snippets/start_reading_modal.html b/bookwyrm/templates/snippets/start_reading_modal.html index 50724e11..239a2be1 100644 --- a/bookwyrm/templates/snippets/start_reading_modal.html +++ b/bookwyrm/templates/snippets/start_reading_modal.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/modal.html' %} +{% extends 'components/modal.html' %} {% block modal-title %} Start "{{ book.title }}" diff --git a/bookwyrm/templates/snippets/status_body.html b/bookwyrm/templates/snippets/status_body.html index c70f898a..69fa0bc2 100644 --- a/bookwyrm/templates/snippets/status_body.html +++ b/bookwyrm/templates/snippets/status_body.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/card.html' %} +{% extends 'components/card.html' %} {% load bookwyrm_tags %} {% load humanize %} diff --git a/bookwyrm/templates/snippets/status_options.html b/bookwyrm/templates/snippets/status_options.html index b5887b1d..a6609cb3 100644 --- a/bookwyrm/templates/snippets/status_options.html +++ b/bookwyrm/templates/snippets/status_options.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/dropdown.html' %} +{% extends 'components/dropdown.html' %} {% load bookwyrm_tags %} {% block dropdown-trigger %} diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html index 2c163034..ab68c2e3 100644 --- a/bookwyrm/templates/snippets/user_options.html +++ b/bookwyrm/templates/snippets/user_options.html @@ -1,4 +1,4 @@ -{% extends 'snippets/components/dropdown.html' %} +{% extends 'components/dropdown.html' %} {% load bookwyrm_tags %} {% block dropdown-trigger %} From 24af288c52115fdff3ae24904a307d34e4e5ce8e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 09:05:53 -0800 Subject: [PATCH 027/280] Refactors user pages --- bookwyrm/templates/{ => user}/followers.html | 24 +++++++++---------- bookwyrm/templates/{ => user}/following.html | 24 +++++++++---------- bookwyrm/templates/{ => user}/user.html | 7 +++--- .../user_layout.html} | 10 ++++++++ bookwyrm/views/user.py | 10 ++++---- 5 files changed, 41 insertions(+), 34 deletions(-) rename bookwyrm/templates/{ => user}/followers.html (69%) rename bookwyrm/templates/{ => user}/following.html (70%) rename bookwyrm/templates/{ => user}/user.html (95%) rename bookwyrm/templates/{snippets/user_header.html => user/user_layout.html} (92%) diff --git a/bookwyrm/templates/followers.html b/bookwyrm/templates/user/followers.html similarity index 69% rename from bookwyrm/templates/followers.html rename to bookwyrm/templates/user/followers.html index 00cb13ca..42b8cfb0 100644 --- a/bookwyrm/templates/followers.html +++ b/bookwyrm/templates/user/followers.html @@ -1,18 +1,17 @@ -{% extends 'layout.html' %} +{% extends 'user/user_layout.html' %} {% load bookwyrm_tags %} -{% block content %} -
    -

    - {% if is_self %}Your - {% else %} - {% include 'snippets/username.html' with user=user possessive=True %} - {% endif %} - followers -

    -
    -{% include 'snippets/user_header.html' with user=user %} +{% block header %} +

    + {% if is_self %}Your + {% else %} + {% include 'snippets/username.html' with user=user possessive=True %} + {% endif %} + followers +

    +{% endblock %} +{% block panel %}

    Followers

    {% for followers in followers %} @@ -34,5 +33,4 @@
    {{ user|username }} has no followers
    {% endif %}
    - {% endblock %} diff --git a/bookwyrm/templates/following.html b/bookwyrm/templates/user/following.html similarity index 70% rename from bookwyrm/templates/following.html rename to bookwyrm/templates/user/following.html index bdf02c74..9e42b783 100644 --- a/bookwyrm/templates/following.html +++ b/bookwyrm/templates/user/following.html @@ -1,18 +1,17 @@ -{% extends 'layout.html' %} +{% extends 'user/user_layout.html' %} {% load bookwyrm_tags %} -{% block content %} -
    -

    - Users following - {% if is_self %}you - {% else %} - {% include 'snippets/username.html' with user=user %} - {% endif %} -

    -
    -{% include 'snippets/user_header.html' with user=user %} +{% block header %} +

    + Users following + {% if is_self %}you + {% else %} + {% include 'snippets/username.html' with user=user %} + {% endif %} +

    +{% endblock %} +{% block panel %}

    Following

    {% for follower in user.following.all %} @@ -34,5 +33,4 @@
    {{ user|username }} isn't following any users
    {% endif %}
    - {% endblock %} diff --git a/bookwyrm/templates/user.html b/bookwyrm/templates/user/user.html similarity index 95% rename from bookwyrm/templates/user.html rename to bookwyrm/templates/user/user.html index 69b762b0..dc3169c2 100644 --- a/bookwyrm/templates/user.html +++ b/bookwyrm/templates/user/user.html @@ -1,6 +1,6 @@ -{% extends 'layout.html' %} -{% block content %} +{% extends 'user/user_layout.html' %} +{% block header %}

    User profile

    @@ -15,8 +15,9 @@
    {% endif %}
    +{% endblock %} -{% include 'snippets/user_header.html' with user=user %} +{% block panel %} {% if user.bookwyrm_user %}

    Shelves

    diff --git a/bookwyrm/templates/snippets/user_header.html b/bookwyrm/templates/user/user_layout.html similarity index 92% rename from bookwyrm/templates/snippets/user_header.html rename to bookwyrm/templates/user/user_layout.html index 8f5e264a..8cf71d9e 100644 --- a/bookwyrm/templates/snippets/user_header.html +++ b/bookwyrm/templates/user/user_layout.html @@ -1,5 +1,12 @@ +{% extends 'layout.html' %} {% load humanize %} {% load bookwyrm_tags %} +
    + {% block header %}{% endblock %} +
    + +{% block content %} +{# user bio #}
    @@ -60,3 +67,6 @@ {% endif %}
    +{% block panel %}{% endblock %} + +{% endblock %} diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 4afda01b..4bbd8e29 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -90,7 +90,7 @@ class User(View): 'goal': goal, } - return TemplateResponse(request, 'user.html', data) + return TemplateResponse(request, 'user/user.html', data) class Followers(View): ''' list of followers view ''' @@ -115,7 +115,7 @@ class Followers(View): 'is_self': request.user.id == user.id, 'followers': user.followers.all(), } - return TemplateResponse(request, 'followers.html', data) + return TemplateResponse(request, 'user/followers.html', data) class Following(View): ''' list of following view ''' @@ -140,7 +140,7 @@ class Following(View): 'is_self': request.user.id == user.id, 'following': user.following.all(), } - return TemplateResponse(request, 'following.html', data) + return TemplateResponse(request, 'user/following.html', data) @method_decorator(login_required, name='dispatch') @@ -153,7 +153,7 @@ class EditUser(View): 'form': forms.EditUserForm(instance=request.user), 'user': request.user, } - return TemplateResponse(request, 'edit_user.html', data) + return TemplateResponse(request, 'settings/edit_user.html', data) def post(self, request): ''' les get fancy with images ''' @@ -161,7 +161,7 @@ class EditUser(View): request.POST, request.FILES, instance=request.user) if not form.is_valid(): data = {'form': form, 'user': request.user} - return TemplateResponse(request, 'edit_user.html', data) + return TemplateResponse(request, 'settings/edit_user.html', data) user = form.save(commit=False) From 9fa8ee3940b7e7560ff93ba865710067105e8153 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 09:28:00 -0800 Subject: [PATCH 028/280] Groups profile settings templates --- bookwyrm/templates/layout.html | 2 +- .../templates/{ => preferences}/blocks.html | 2 +- .../{ => preferences}/change_password.html | 2 +- .../{ => preferences}/edit_user.html | 2 +- .../{ => preferences}/preferences_layout.html | 6 ++--- bookwyrm/templates/user/user.html | 2 +- bookwyrm/urls.py | 13 +++++----- bookwyrm/views/__init__.py | 1 + bookwyrm/views/block.py | 6 ++--- bookwyrm/views/password.py | 7 ++--- bookwyrm/views/rss_feed.py | 26 ++++++++++++------- bookwyrm/views/user.py | 4 +-- 12 files changed, 41 insertions(+), 32 deletions(-) rename bookwyrm/templates/{ => preferences}/blocks.html (91%) rename bookwyrm/templates/{ => preferences}/change_password.html (93%) rename bookwyrm/templates/{ => preferences}/edit_user.html (96%) rename bookwyrm/templates/{ => preferences}/preferences_layout.html (57%) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index ddbafbb4..a696e287 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -81,7 +81,7 @@
  • - + Settings
  • diff --git a/bookwyrm/templates/blocks.html b/bookwyrm/templates/preferences/blocks.html similarity index 91% rename from bookwyrm/templates/blocks.html rename to bookwyrm/templates/preferences/blocks.html index 1df49816..5fd29d25 100644 --- a/bookwyrm/templates/blocks.html +++ b/bookwyrm/templates/preferences/blocks.html @@ -1,4 +1,4 @@ -{% extends 'preferences_layout.html' %} +{% extends 'preferences/preferences_layout.html' %} {% block header %} Blocked Users diff --git a/bookwyrm/templates/change_password.html b/bookwyrm/templates/preferences/change_password.html similarity index 93% rename from bookwyrm/templates/change_password.html rename to bookwyrm/templates/preferences/change_password.html index c373dfc8..b1b2a049 100644 --- a/bookwyrm/templates/change_password.html +++ b/bookwyrm/templates/preferences/change_password.html @@ -1,4 +1,4 @@ -{% extends 'preferences_layout.html' %} +{% extends 'preferences/preferences_layout.html' %} {% block header %} Change Password {% endblock %} diff --git a/bookwyrm/templates/edit_user.html b/bookwyrm/templates/preferences/edit_user.html similarity index 96% rename from bookwyrm/templates/edit_user.html rename to bookwyrm/templates/preferences/edit_user.html index ee9ddb22..8b384276 100644 --- a/bookwyrm/templates/edit_user.html +++ b/bookwyrm/templates/preferences/edit_user.html @@ -1,4 +1,4 @@ -{% extends 'preferences_layout.html' %} +{% extends 'preferences/preferences_layout.html' %} {% block header %} Edit Profile {% endblock %} diff --git a/bookwyrm/templates/preferences_layout.html b/bookwyrm/templates/preferences/preferences_layout.html similarity index 57% rename from bookwyrm/templates/preferences_layout.html rename to bookwyrm/templates/preferences/preferences_layout.html index de2fe0df..b0e7b31a 100644 --- a/bookwyrm/templates/preferences_layout.html +++ b/bookwyrm/templates/preferences/preferences_layout.html @@ -10,16 +10,16 @@ diff --git a/bookwyrm/templates/user/user.html b/bookwyrm/templates/user/user.html index dc3169c2..48f15398 100644 --- a/bookwyrm/templates/user/user.html +++ b/bookwyrm/templates/user/user.html @@ -7,7 +7,7 @@
    {% if is_self %}
    - + Edit profile diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index f03bcda9..e3b792ff 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -5,7 +5,6 @@ from django.urls import path, re_path from bookwyrm import incoming, settings, views, wellknown -from bookwyrm.views.rss_feed import RssFeed from bookwyrm.utils import regex user_path = r'^user/(?P%s)' % regex.username @@ -49,7 +48,6 @@ urlpatterns = [ re_path(r'^password-reset/?$', views.PasswordResetRequest.as_view()), re_path(r'^password-reset/(?P[A-Za-z0-9]+)/?$', views.PasswordReset.as_view()), - re_path(r'^change-password/?$', views.ChangePassword.as_view()), # invites re_path(r'^invite/?$', views.ManageInvites.as_view()), @@ -76,9 +74,15 @@ urlpatterns = [ re_path(r'%s/shelves/?$' % user_path, views.user_shelves_page), re_path(r'%s/followers(.json)?/?$' % user_path, views.Followers.as_view()), re_path(r'%s/following(.json)?/?$' % user_path, views.Following.as_view()), - re_path(r'^edit-profile/?$', views.EditUser.as_view()), re_path(r'%s/rss' % user_path, views.rss_feed.RssFeed()), + # preferences + re_path(r'^preferences/profile/?$', views.EditUser.as_view()), + re_path(r'^preferences/password/?$', views.ChangePassword.as_view()), + re_path(r'^preferences/block/?$', views.Block.as_view()), + re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), + re_path(r'^unblock/(?P\d+)/?$', views.unblock), + # reading goals re_path(r'%s/goal/(?P\d{4})/?$' % user_path, views.Goal.as_view()), @@ -140,7 +144,4 @@ urlpatterns = [ re_path(r'^accept-follow-request/?$', views.accept_follow_request), re_path(r'^delete-follow-request/?$', views.delete_follow_request), - re_path(r'^block/?$', views.Block.as_view()), - re_path(r'^block/(?P\d+)/?$', views.Block.as_view()), - re_path(r'^unblock/(?P\d+)/?$', views.unblock), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index e3ac29c8..36a4bed9 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -17,6 +17,7 @@ from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough from .reading import start_reading, finish_reading, delete_progressupdate +from .rss_feed import RssFeed from .password import PasswordResetRequest, PasswordReset, ChangePassword from .tag import Tag, AddTag, RemoveTag from .search import Search diff --git a/bookwyrm/views/block.py b/bookwyrm/views/block.py index fb95479a..ebcced2a 100644 --- a/bookwyrm/views/block.py +++ b/bookwyrm/views/block.py @@ -17,7 +17,7 @@ class Block(View): def get(self, request): ''' list of blocked users? ''' return TemplateResponse( - request, 'blocks.html', {'title': 'Blocked Users'}) + request, 'preferences/blocks.html', {'title': 'Blocked Users'}) def post(self, request, user_id): ''' block a user ''' @@ -31,7 +31,7 @@ class Block(View): privacy='direct', direct_recipients=[to_block] ) - return redirect('/block') + return redirect('/preferences/block') @require_POST @@ -55,4 +55,4 @@ def unblock(request, user_id): direct_recipients=[to_unblock] ) block.delete() - return redirect('/block') + return redirect('/preferences/block') diff --git a/bookwyrm/views/password.py b/bookwyrm/views/password.py index 06ddc1da..2adeebf5 100644 --- a/bookwyrm/views/password.py +++ b/bookwyrm/views/password.py @@ -94,7 +94,8 @@ class ChangePassword(View): 'title': 'Change Password', 'user': request.user, } - return TemplateResponse(request, 'change_password.html', data) + return TemplateResponse( + request, 'preferences/change_password.html', data) def post(self, request): ''' allow a user to change their password ''' @@ -102,9 +103,9 @@ class ChangePassword(View): confirm_password = request.POST.get('confirm-password') if new_password != confirm_password: - return redirect('/edit-profile') + return redirect('preferences/password') request.user.set_password(new_password) request.user.save() login(request, request.user) - return redirect('/user/%s' % request.user.localname) + return redirect(request.user.local_path) diff --git a/bookwyrm/views/rss_feed.py b/bookwyrm/views/rss_feed.py index d6bcd174..496689ff 100644 --- a/bookwyrm/views/rss_feed.py +++ b/bookwyrm/views/rss_feed.py @@ -1,29 +1,35 @@ -''' ''' +''' serialize user's posts in rss feed ''' from django.contrib.syndication.views import Feed -from django.urls import reverse -from bookwyrm.models.user import User from .helpers import get_activity_feed, get_user_from_username +# pylint: disable=no-self-use, unused-argument class RssFeed(Feed): - - description_template = "snippets/rss_content.html" - title_template = "snippets/rss_title.html" + ''' serialize user's posts in rss feed ''' + description_template = 'snippets/rss_content.html' + title_template = 'snippets/rss_title.html' def get_object(self, request, username): + ''' the user who's posts get serialized ''' return get_user_from_username(username) + def link(self, obj): + ''' link to the user's profile ''' return obj.local_path + def title(self, obj): - return f"Status updates from {obj.display_name}" + ''' title of the rss feed entry ''' + return f'Status updates from {obj.display_name}' def items(self, obj): - return get_activity_feed(obj, ['public', 'unlisted'], queryset=obj.status_set) + ''' the user's activity feed ''' + return get_activity_feed( + obj, ['public', 'unlisted'], queryset=obj.status_set) + - def item_link(self, item): + ''' link to the status ''' return item.local_path - diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 4bbd8e29..a06face2 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -153,7 +153,7 @@ class EditUser(View): 'form': forms.EditUserForm(instance=request.user), 'user': request.user, } - return TemplateResponse(request, 'settings/edit_user.html', data) + return TemplateResponse(request, 'preferences/edit_user.html', data) def post(self, request): ''' les get fancy with images ''' @@ -161,7 +161,7 @@ class EditUser(View): request.POST, request.FILES, instance=request.user) if not form.is_valid(): data = {'form': form, 'user': request.user} - return TemplateResponse(request, 'settings/edit_user.html', data) + return TemplateResponse(request, 'preferences/edit_user.html', data) user = form.save(commit=False) From 60721fd41ea6aafe21717321c21b77baf59e9dce Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 09:29:59 -0800 Subject: [PATCH 029/280] Fixes user layout --- bookwyrm/templates/user/user_layout.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/user/user_layout.html b/bookwyrm/templates/user/user_layout.html index 8cf71d9e..3fcbf1e2 100644 --- a/bookwyrm/templates/user/user_layout.html +++ b/bookwyrm/templates/user/user_layout.html @@ -1,11 +1,12 @@ {% extends 'layout.html' %} {% load humanize %} {% load bookwyrm_tags %} + +{% block content %}
    {% block header %}{% endblock %}
    -{% block content %} {# user bio #}
    diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index 931cf355..08a5e057 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -44,6 +44,7 @@ class Feed(View): 'activities': paginated.page(page), 'tab': tab, 'goal_form': forms.GoalForm(), + 'path': '/%s' % tab, }} return TemplateResponse(request, 'feed/feed.html', data) @@ -65,6 +66,7 @@ class DirectMessage(View): 'title': 'Direct Messages', 'user': request.user, 'activities': activity_page, + 'path': '/direct-messages', }} return TemplateResponse(request, 'feed/direct_messages.html', data) From 93ef9e31adb6bf2e8dbaf031b68255583c9664cb Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 11:44:04 -0800 Subject: [PATCH 036/280] Send message to a particular user --- bookwyrm/templates/feed/direct_messages.html | 37 ++++++++++--------- .../snippets/create_status_form.html | 2 +- .../templates/snippets/status_options.html | 3 ++ bookwyrm/templates/snippets/user_options.html | 3 ++ bookwyrm/templates/user/user_layout.html | 2 +- bookwyrm/urls.py | 2 + bookwyrm/views/feed.py | 19 +++++++++- 7 files changed, 46 insertions(+), 22 deletions(-) diff --git a/bookwyrm/templates/feed/direct_messages.html b/bookwyrm/templates/feed/direct_messages.html index 1f41808f..8c53cbeb 100644 --- a/bookwyrm/templates/feed/direct_messages.html +++ b/bookwyrm/templates/feed/direct_messages.html @@ -1,25 +1,26 @@ {% extends 'feed/feed_layout.html' %} {% block panel %} -
    -

    Direct Messages

    +
    +

    Direct Messages{% if partner %} with {% include 'snippets/username.html' with user=partner %}{% endif %}

    + {% if partner %}

    All messages

    {% endif %} +
    -
    - {% include 'snippets/create_status_form.html' with type="direct" uuid=1 %} -
    - -
    - {% if not activities %} -

    You have no messages right now.

    - {% endif %} - {% for activity in activities %} -
    - {% include 'snippets/status.html' with status=activity %} -
    - {% endfor %} - - {% include 'snippets/pagination.html' with page=activities path="direct-messages" %} -
    +
    + {% include 'snippets/create_status_form.html' with type="direct" uuid=1 mentions=partner %}
    +
    + {% if not activities %} +

    You have no messages right now.

    + {% endif %} + {% for activity in activities %} +
    + {% include 'snippets/status.html' with status=activity %} +
    + {% endfor %} + + {% include 'snippets/pagination.html' with page=activities path="direct-messages" %} +
    + {% endblock %} diff --git a/bookwyrm/templates/snippets/create_status_form.html b/bookwyrm/templates/snippets/create_status_form.html index 534ace29..c6d7be3f 100644 --- a/bookwyrm/templates/snippets/create_status_form.html +++ b/bookwyrm/templates/snippets/create_status_form.html @@ -35,7 +35,7 @@ {% else %} {% include 'snippets/content_warning_field.html' with parent_status=status %} - + {% endif %}
    {% if type == 'quotation' %} diff --git a/bookwyrm/templates/snippets/status_options.html b/bookwyrm/templates/snippets/status_options.html index a6609cb3..3bf8251f 100644 --- a/bookwyrm/templates/snippets/status_options.html +++ b/bookwyrm/templates/snippets/status_options.html @@ -18,6 +18,9 @@ {% else %} +
  • + Send direct message +
  • {% include 'snippets/block_button.html' with user=status.user class="is-fullwidth" %}
  • diff --git a/bookwyrm/templates/snippets/user_options.html b/bookwyrm/templates/snippets/user_options.html index ab68c2e3..bc54ca1c 100644 --- a/bookwyrm/templates/snippets/user_options.html +++ b/bookwyrm/templates/snippets/user_options.html @@ -8,6 +8,9 @@ {% endblock %} {% block dropdown-list %} +
  • + Send direct message +
  • {% include 'snippets/block_button.html' with user=user class="is-fullwidth" %}
  • diff --git a/bookwyrm/templates/user/user_layout.html b/bookwyrm/templates/user/user_layout.html index 3fcbf1e2..1ab51ce3 100644 --- a/bookwyrm/templates/user/user_layout.html +++ b/bookwyrm/templates/user/user_layout.html @@ -42,7 +42,7 @@ {% endif %}
    From 932acc961f2b2ef901d827bf8d38da2037625b8b Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 09:41:32 -0800 Subject: [PATCH 030/280] Fixes template paths in tests --- bookwyrm/tests/views/test_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 07acc4c4..61fcdb64 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -34,7 +34,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'user.html') + self.assertEqual(result.template_name, 'user/user.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -65,7 +65,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'followers.html') + self.assertEqual(result.template_name, 'user/followers.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -96,7 +96,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'following.html') + self.assertEqual(result.template_name, 'user/following.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -125,7 +125,7 @@ class UserViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'edit_user.html') + self.assertEqual(result.template_name, 'preferences/edit_user.html') self.assertEqual(result.status_code, 200) From b53ef73fafcce2ea5abd65d1441f6428c1ffed19 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 10:25:31 -0800 Subject: [PATCH 031/280] Feed templates --- bookwyrm/static/js/shared.js | 9 + .../templates/{ => feed}/direct_messages.html | 4 +- bookwyrm/templates/feed/feed.html | 39 +++++ .../{feed.html => feed/feed_layout.html} | 39 +---- bookwyrm/templates/feed/status.html | 13 ++ .../templates/{snippets => feed}/thread.html | 0 bookwyrm/templates/status.html | 9 - bookwyrm/urls.py | 4 +- bookwyrm/views/__init__.py | 6 +- bookwyrm/views/direct_message.py | 26 --- bookwyrm/views/feed.py | 156 ++++++++++++++++++ bookwyrm/views/landing.py | 71 +------- bookwyrm/views/status.py | 54 +----- 13 files changed, 234 insertions(+), 196 deletions(-) rename bookwyrm/templates/{ => feed}/direct_messages.html (88%) create mode 100644 bookwyrm/templates/feed/feed.html rename bookwyrm/templates/{feed.html => feed/feed_layout.html} (73%) create mode 100644 bookwyrm/templates/feed/status.html rename bookwyrm/templates/{snippets => feed}/thread.html (100%) delete mode 100644 bookwyrm/templates/status.html delete mode 100644 bookwyrm/views/direct_message.py create mode 100644 bookwyrm/views/feed.py diff --git a/bookwyrm/static/js/shared.js b/bookwyrm/static/js/shared.js index a0c21bec..c2fa8b54 100644 --- a/bookwyrm/static/js/shared.js +++ b/bookwyrm/static/js/shared.js @@ -35,8 +35,17 @@ window.onload = function() { // polling document.querySelectorAll('[data-poll]') .forEach(el => polling(el)); + + // browser back behavior + document.querySelectorAll('[data-back]') + .forEach(t => t.onclick = back); }; +function back(e) { + e.preventDefault(); + history.back(); +} + function polling(el) { let delay = 10000 + (Math.random() * 1000); setTimeout(function() { diff --git a/bookwyrm/templates/direct_messages.html b/bookwyrm/templates/feed/direct_messages.html similarity index 88% rename from bookwyrm/templates/direct_messages.html rename to bookwyrm/templates/feed/direct_messages.html index 666f5290..44a0cded 100644 --- a/bookwyrm/templates/direct_messages.html +++ b/bookwyrm/templates/feed/direct_messages.html @@ -1,5 +1,5 @@ -{% extends 'layout.html' %} -{% block content %} +{% extends 'feed/feed_layout.html' %} +{% block panel %}

    Direct Messages

    diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html new file mode 100644 index 00000000..8d6152e2 --- /dev/null +++ b/bookwyrm/templates/feed/feed.html @@ -0,0 +1,39 @@ +{% extends 'feed/feed_layout.html' %} +{% load bookwyrm_tags %} +{% block panel %} + +

    {{ tab | title }} Timeline

    +
    + +
    + +{# announcements and system messages #} +{% if not goal and tab == 'home' %} +{% now 'Y' as year %} + +{% endif %} + +{# activity feed #} +{% if not activities %} +

    There aren't any activities right now! Try following a user to get started

    +{% endif %} +{% for activity in activities %} +
    +{% include 'snippets/status.html' with status=activity %} +
    +{% endfor %} + +{% endblock %} diff --git a/bookwyrm/templates/feed.html b/bookwyrm/templates/feed/feed_layout.html similarity index 73% rename from bookwyrm/templates/feed.html rename to bookwyrm/templates/feed/feed_layout.html index 1368660b..33123ca8 100644 --- a/bookwyrm/templates/feed.html +++ b/bookwyrm/templates/feed/feed_layout.html @@ -3,6 +3,7 @@ {% block content %}
    + {% if user.is_authenticated %}

    Your books

    {% if not suggested_books %} @@ -69,43 +70,15 @@ {% endif %}
    + {% endif %}
    -

    {{ tab | title }} Timeline

    -
    - -
    - - {# announcements and system messages #} - {% if not goal and tab == 'home' %} - {% now 'Y' as year %} - - {% endif %} - - {# activity feed #} - {% if not activities %} -

    There aren't any activities right now! Try following a user to get started

    - {% endif %} - {% for activity in activities %} -
    - {% include 'snippets/status.html' with status=activity %} -
    - {% endfor %} + {% block panel %}{% endblock %} + {% if activities %} {% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %} + {% endif %}
    {% endblock %} + diff --git a/bookwyrm/templates/feed/status.html b/bookwyrm/templates/feed/status.html new file mode 100644 index 00000000..eb848993 --- /dev/null +++ b/bookwyrm/templates/feed/status.html @@ -0,0 +1,13 @@ +{% extends 'feed/feed_layout.html' %} +{% block panel %} +
    + + + Back + +
    + +{% include 'feed/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} + +{% endblock %} + diff --git a/bookwyrm/templates/snippets/thread.html b/bookwyrm/templates/feed/thread.html similarity index 100% rename from bookwyrm/templates/snippets/thread.html rename to bookwyrm/templates/feed/thread.html diff --git a/bookwyrm/templates/status.html b/bookwyrm/templates/status.html deleted file mode 100644 index d38ed89e..00000000 --- a/bookwyrm/templates/status.html +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'layout.html' %} -{% block content %} - -
    - {% include 'snippets/thread.html' with status=status depth=0 max_depth=6 is_root=True direction=0 %} -
    - -{% endblock %} - diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index e3b792ff..d232747f 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -56,9 +56,11 @@ urlpatterns = [ # landing pages re_path(r'^about/?$', views.About.as_view()), path('', views.Home.as_view()), - re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), re_path(r'^discover/?$', views.Discover.as_view()), re_path(r'^notifications/?$', views.Notifications.as_view()), + + # feeds + re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), re_path(r'^direct-messages/?$', views.DirectMessage.as_view()), # search diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 36a4bed9..80520eb3 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -4,15 +4,15 @@ from .author import Author, EditAuthor from .block import Block, unblock from .books import Book, EditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book -from .direct_message import DirectMessage from .error import not_found_page, server_error_page +from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request, handle_accept from .goal import Goal from .import_data import Import, ImportStatus from .interaction import Favorite, Unfavorite, Boost, Unboost from .invite import ManageInvites, Invite -from .landing import About, Home, Feed, Discover +from .landing import About, Home, Discover from .notifications import Notifications from .outbox import Outbox from .reading import edit_readthrough, create_readthrough, delete_readthrough @@ -24,6 +24,6 @@ from .search import Search from .shelf import Shelf from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import shelve, unshelve -from .status import Status, Replies, CreateStatus, DeleteStatus +from .status import CreateStatus, DeleteStatus from .updates import Updates from .user import User, EditUser, Followers, Following diff --git a/bookwyrm/views/direct_message.py b/bookwyrm/views/direct_message.py deleted file mode 100644 index 1f6c4f19..00000000 --- a/bookwyrm/views/direct_message.py +++ /dev/null @@ -1,26 +0,0 @@ -''' non-interactive pages ''' -from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator -from django.template.response import TemplateResponse -from django.utils.decorators import method_decorator -from django.views import View - -from bookwyrm.settings import PAGE_LENGTH -from .helpers import get_activity_feed - - -# pylint: disable= no-self-use -@method_decorator(login_required, name='dispatch') -class DirectMessage(View): - ''' dm view ''' - def get(self, request, page=1): - ''' like a feed but for dms only ''' - activities = get_activity_feed(request.user, 'direct') - paginated = Paginator(activities, PAGE_LENGTH) - activity_page = paginated.page(page) - data = { - 'title': 'Direct Messages', - 'user': request.user, - 'activities': activity_page, - } - return TemplateResponse(request, 'direct_messages.html', data) diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py new file mode 100644 index 00000000..931cf355 --- /dev/null +++ b/bookwyrm/views/feed.py @@ -0,0 +1,156 @@ +''' non-interactive pages ''' +from django.contrib.auth.decorators import login_required +from django.core.paginator import Paginator +from django.http import HttpResponseNotFound +from django.template.response import TemplateResponse +from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import forms, models +from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.settings import PAGE_LENGTH +from .helpers import get_activity_feed +from .helpers import get_user_from_username +from .helpers import is_api_request, is_bookworm_request, object_visible_to_user + + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +class Feed(View): + ''' activity stream ''' + def get(self, request, tab): + ''' user's homepage with activity feed ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + if tab == 'home': + activities = get_activity_feed( + request.user, ['public', 'unlisted', 'followers'], + following_only=True) + elif tab == 'local': + activities = get_activity_feed( + request.user, ['public', 'followers'], local_only=True) + else: + activities = get_activity_feed( + request.user, ['public', 'followers']) + paginated = Paginator(activities, PAGE_LENGTH) + + data = {**feed_page_data(request.user), **{ + 'title': 'Updates Feed', + 'user': request.user, + 'activities': paginated.page(page), + 'tab': tab, + 'goal_form': forms.GoalForm(), + }} + return TemplateResponse(request, 'feed/feed.html', data) + + +@method_decorator(login_required, name='dispatch') +class DirectMessage(View): + ''' dm view ''' + def get(self, request): + ''' like a feed but for dms only ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + activities = get_activity_feed(request.user, 'direct') + paginated = Paginator(activities, PAGE_LENGTH) + activity_page = paginated.page(page) + data = {**feed_page_data(request.user), **{ + 'title': 'Direct Messages', + 'user': request.user, + 'activities': activity_page, + }} + return TemplateResponse(request, 'feed/direct_messages.html', data) + + +class Status(View): + ''' get posting ''' + def get(self, request, username, status_id): + ''' display a particular status (and replies, etc) ''' + try: + user = get_user_from_username(username) + status = models.Status.objects.select_subclasses().get( + id=status_id, deleted=False) + except ValueError: + return HttpResponseNotFound() + + # the url should have the poster's username in it + if user != status.user: + return HttpResponseNotFound() + + # make sure the user is authorized to see the status + if not object_visible_to_user(request.user, status): + return HttpResponseNotFound() + + if is_api_request(request): + return ActivitypubResponse( + status.to_activity(pure=not is_bookworm_request(request))) + + data = {**feed_page_data(request.user), **{ + 'title': 'Status by %s' % user.username, + 'status': status, + }} + return TemplateResponse(request, 'feed/status.html', data) + + +class Replies(View): + ''' replies page (a json view of status) ''' + def get(self, request, username, status_id): + ''' ordered collection of replies to a status ''' + # the html view is the same as Status + if not is_api_request(request): + status_view = Status.as_view() + return status_view(request, username, status_id) + + # the json view is different than Status + status = models.Status.objects.get(id=status_id) + if status.user.localname != username: + return HttpResponseNotFound() + + return ActivitypubResponse(status.to_replies(**request.GET)) + + +def feed_page_data(user): + ''' info we need for every feed page ''' + if not user.is_authenticated: + return {} + + goal = models.AnnualGoal.objects.filter( + user=user, year=timezone.now().year + ).first() + return { + 'suggested_books': get_suggested_books(user), + 'goal': goal, + 'goal_form': forms.GoalForm(), + } + +def get_suggested_books(user, max_books=5): + ''' helper to get a user's recent books ''' + book_count = 0 + preset_shelves = [ + ('reading', max_books), ('read', 2), ('to-read', max_books) + ] + suggested_books = [] + for (preset, shelf_max) in preset_shelves: + limit = shelf_max if shelf_max < (max_books - book_count) \ + else max_books - book_count + shelf = user.shelf_set.get(identifier=preset) + + shelf_books = shelf.shelfbook_set.order_by( + '-updated_date' + ).all()[:limit] + if not shelf_books: + continue + shelf_preview = { + 'name': shelf.name, + 'books': [s.book for s in shelf_books] + } + suggested_books.append(shelf_preview) + book_count += len(shelf_preview['books']) + return suggested_books diff --git a/bookwyrm/views/landing.py b/bookwyrm/views/landing.py index ec6cb3a9..0d841ef0 100644 --- a/bookwyrm/views/landing.py +++ b/bookwyrm/views/landing.py @@ -1,14 +1,10 @@ ''' non-interactive pages ''' -from django.contrib.auth.decorators import login_required -from django.core.paginator import Paginator from django.db.models import Avg, Max from django.template.response import TemplateResponse -from django.utils import timezone -from django.utils.decorators import method_decorator from django.views import View from bookwyrm import forms, models -from bookwyrm.settings import PAGE_LENGTH +from .feed import Feed from .helpers import get_activity_feed @@ -61,68 +57,3 @@ class Discover(View): 'ratings': ratings } return TemplateResponse(request, 'discover.html', data) - - -@method_decorator(login_required, name='dispatch') -class Feed(View): - ''' activity stream ''' - def get(self, request, tab): - ''' user's homepage with activity feed ''' - try: - page = int(request.GET.get('page', 1)) - except ValueError: - page = 1 - - suggested_books = get_suggested_books(request.user) - - if tab == 'home': - activities = get_activity_feed( - request.user, ['public', 'unlisted', 'followers'], - following_only=True) - elif tab == 'local': - activities = get_activity_feed( - request.user, ['public', 'followers'], local_only=True) - else: - activities = get_activity_feed( - request.user, ['public', 'followers']) - paginated = Paginator(activities, PAGE_LENGTH) - - goal = models.AnnualGoal.objects.filter( - user=request.user, year=timezone.now().year - ).first() - data = { - 'title': 'Updates Feed', - 'user': request.user, - 'suggested_books': suggested_books, - 'activities': paginated.page(page), - 'tab': tab, - 'goal': goal, - 'goal_form': forms.GoalForm(), - } - return TemplateResponse(request, 'feed.html', data) - - -def get_suggested_books(user, max_books=5): - ''' helper to get a user's recent books ''' - book_count = 0 - preset_shelves = [ - ('reading', max_books), ('read', 2), ('to-read', max_books) - ] - suggested_books = [] - for (preset, shelf_max) in preset_shelves: - limit = shelf_max if shelf_max < (max_books - book_count) \ - else max_books - book_count - shelf = user.shelf_set.get(identifier=preset) - - shelf_books = shelf.shelfbook_set.order_by( - '-updated_date' - ).all()[:limit] - if not shelf_books: - continue - shelf_preview = { - 'name': shelf.name, - 'books': [s.book for s in shelf_books] - } - suggested_books.append(shelf_preview) - book_count += len(shelf_preview['books']) - return suggested_books diff --git a/bookwyrm/views/status.py b/bookwyrm/views/status.py index 834cf583..4d342bfb 100644 --- a/bookwyrm/views/status.py +++ b/bookwyrm/views/status.py @@ -1,55 +1,22 @@ ''' what are we here for if not for posting ''' import re from django.contrib.auth.decorators import login_required -from django.http import HttpResponseBadRequest, HttpResponseNotFound +from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect -from django.template.response import TemplateResponse from django.utils.decorators import method_decorator from django.views import View from markdown import markdown from bookwyrm import forms, models -from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.broadcast import broadcast from bookwyrm.sanitize_html import InputHtmlParser from bookwyrm.settings import DOMAIN from bookwyrm.status import create_notification, delete_status from bookwyrm.utils import regex -from .helpers import get_user_from_username, handle_remote_webfinger -from .helpers import is_api_request, is_bookworm_request, object_visible_to_user +from .helpers import handle_remote_webfinger # pylint: disable= no-self-use -class Status(View): - ''' get posting ''' - def get(self, request, username, status_id): - ''' display a particular status (and replies, etc) ''' - try: - user = get_user_from_username(username) - status = models.Status.objects.select_subclasses().get( - id=status_id, deleted=False) - except ValueError: - return HttpResponseNotFound() - - # the url should have the poster's username in it - if user != status.user: - return HttpResponseNotFound() - - # make sure the user is authorized to see the status - if not object_visible_to_user(request.user, status): - return HttpResponseNotFound() - - if is_api_request(request): - return ActivitypubResponse( - status.to_activity(pure=not is_bookworm_request(request))) - - data = { - 'title': 'Status by %s' % user.username, - 'status': status, - } - return TemplateResponse(request, 'status.html', data) - - @method_decorator(login_required, name='dispatch') class CreateStatus(View): ''' the view for *posting* ''' @@ -144,23 +111,6 @@ class DeleteStatus(View): broadcast(request.user, status.to_delete_activity(request.user)) return redirect(request.headers.get('Referer', '/')) - -class Replies(View): - ''' replies page (a json view of status) ''' - def get(self, request, username, status_id): - ''' ordered collection of replies to a status ''' - # the html view is the same as Status - if not is_api_request(request): - status_view = Status.as_view() - return status_view(request, username, status_id) - - # the json view is different than Status - status = models.Status.objects.get(id=status_id) - if status.user.localname != username: - return HttpResponseNotFound() - - return ActivitypubResponse(status.to_replies(**request.GET)) - def find_mentions(content): ''' detect @mentions in raw status content ''' for match in re.finditer(regex.strict_username, content): From cc5fedb1d104aa32d929b2c32beb42cec3d88e58 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 10:29:21 -0800 Subject: [PATCH 032/280] Fixes template paths in views --- bookwyrm/tests/views/test_block.py | 2 +- bookwyrm/tests/views/test_direct_message.py | 2 +- bookwyrm/tests/views/test_landing.py | 4 ++-- bookwyrm/tests/views/test_status.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py index fa5254d1..f3a0c2f8 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/test_block.py @@ -32,7 +32,7 @@ class BlockViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'blocks.html') + self.assertEqual(result.template_name, 'preferences/blocks.html') self.assertEqual(result.status_code, 200) def test_block_post(self): diff --git a/bookwyrm/tests/views/test_direct_message.py b/bookwyrm/tests/views/test_direct_message.py index 48820c75..0207ece0 100644 --- a/bookwyrm/tests/views/test_direct_message.py +++ b/bookwyrm/tests/views/test_direct_message.py @@ -24,5 +24,5 @@ class DirectMessageViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'direct_messages.html') + self.assertEqual(result.template_name, 'feed/direct_messages.html') self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index 5596b4f3..f1429e5a 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -31,7 +31,7 @@ class LandingViews(TestCase): request.user = self.local_user result = view(request) self.assertEqual(result.status_code, 200) - self.assertEqual(result.template_name, 'feed.html') + self.assertEqual(result.template_name, 'feed/feed.html') request.user = self.anonymous_user result = view(request) @@ -58,7 +58,7 @@ class LandingViews(TestCase): request.user = self.local_user result = view(request, 'local') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed.html') + self.assertEqual(result.template_name, 'feed/feed.html') self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index 50c458ac..b60feaa8 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -47,7 +47,7 @@ class StatusViews(TestCase): is_api.return_value = False result = view(request, 'mouse', status.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'status.html') + self.assertEqual(result.template_name, 'feed/status.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.status.is_api_request') as is_api: @@ -68,7 +68,7 @@ class StatusViews(TestCase): is_api.return_value = False result = view(request, 'mouse', status.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'status.html') + self.assertEqual(result.template_name, 'feed/status.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.status.is_api_request') as is_api: From bae54b6847d67e8972c052a960b72d962f07a5c0 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 10:31:55 -0800 Subject: [PATCH 033/280] Shuffles tests around for feed views --- bookwyrm/tests/views/test_direct_message.py | 28 --------------------- bookwyrm/tests/views/test_landing.py | 27 -------------------- 2 files changed, 55 deletions(-) delete mode 100644 bookwyrm/tests/views/test_direct_message.py diff --git a/bookwyrm/tests/views/test_direct_message.py b/bookwyrm/tests/views/test_direct_message.py deleted file mode 100644 index 0207ece0..00000000 --- a/bookwyrm/tests/views/test_direct_message.py +++ /dev/null @@ -1,28 +0,0 @@ -''' test for app action functionality ''' -from django.template.response import TemplateResponse -from django.test import TestCase -from django.test.client import RequestFactory - -from bookwyrm import models -from bookwyrm import views - - -class DirectMessageViews(TestCase): - ''' dms ''' - def setUp(self): - ''' we need basic test data and mocks ''' - self.factory = RequestFactory() - self.local_user = models.User.objects.create_user( - 'mouse@local.com', 'mouse@mouse.mouse', 'password', - local=True, localname='mouse') - - - def test_direct_messages_page(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.DirectMessage.as_view() - request = self.factory.get('') - request.user = self.local_user - result = view(request) - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/direct_messages.html') - self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index f1429e5a..8576af49 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -18,10 +18,6 @@ class LandingViews(TestCase): local=True, localname='mouse') self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False - self.book = models.Edition.objects.create( - title='Example Edition', - remote_id='https://example.com/book/1', - ) def test_home_page(self): @@ -51,17 +47,6 @@ class LandingViews(TestCase): self.assertEqual(result.status_code, 200) - def test_feed(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.Feed.as_view() - request = self.factory.get('') - request.user = self.local_user - result = view(request, 'local') - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/feed.html') - self.assertEqual(result.status_code, 200) - - def test_discover(self): ''' there are so many views, this just makes sure it LOADS ''' view = views.Discover.as_view() @@ -70,15 +55,3 @@ class LandingViews(TestCase): self.assertIsInstance(result, TemplateResponse) self.assertEqual(result.template_name, 'discover.html') self.assertEqual(result.status_code, 200) - - - def test_get_suggested_book(self): - ''' gets books the ~*~ algorithm ~*~ thinks you want to post about ''' - models.ShelfBook.objects.create( - book=self.book, - added_by=self.local_user, - shelf=self.local_user.shelf_set.get(identifier='reading') - ) - suggestions = views.landing.get_suggested_books(self.local_user) - self.assertEqual(suggestions[0]['name'], 'Currently Reading') - self.assertEqual(suggestions[0]['books'][0], self.book) From 640db7e9e31857280c74ddee6834d6fe95b15c9e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 10:44:21 -0800 Subject: [PATCH 034/280] Fixes more tests for moved-around views --- bookwyrm/tests/views/test_feed.py | 99 +++++++++++++++++++++++++++ bookwyrm/tests/views/test_password.py | 2 +- bookwyrm/tests/views/test_status.py | 42 ------------ 3 files changed, 100 insertions(+), 43 deletions(-) create mode 100644 bookwyrm/tests/views/test_feed.py diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py new file mode 100644 index 00000000..da597a7c --- /dev/null +++ b/bookwyrm/tests/views/test_feed.py @@ -0,0 +1,99 @@ +''' test for app action functionality ''' +from unittest.mock import patch +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models +from bookwyrm import views +from bookwyrm.activitypub import ActivitypubResponse + + +class FeedMessageViews(TestCase): + ''' dms ''' + def setUp(self): + ''' we need basic test data and mocks ''' + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + 'mouse@local.com', 'mouse@mouse.mouse', 'password', + local=True, localname='mouse') + self.book = models.Edition.objects.create( + title='Example Edition', + remote_id='https://example.com/book/1', + ) + + + def test_feed(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Feed.as_view() + request = self.factory.get('') + request.user = self.local_user + result = view(request, 'local') + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'feed/feed.html') + self.assertEqual(result.status_code, 200) + + + def test_status_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Status.as_view() + status = models.Status.objects.create( + content='hi', user=self.local_user) + request = self.factory.get('') + request.user = self.local_user + with patch('bookwyrm.views.feed.is_api_request') as is_api: + is_api.return_value = False + result = view(request, 'mouse', status.id) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'feed/status.html') + self.assertEqual(result.status_code, 200) + + with patch('bookwyrm.views.feed.is_api_request') as is_api: + is_api.return_value = True + result = view(request, 'mouse', status.id) + self.assertIsInstance(result, ActivitypubResponse) + self.assertEqual(result.status_code, 200) + + + def test_replies_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Replies.as_view() + status = models.Status.objects.create( + content='hi', user=self.local_user) + request = self.factory.get('') + request.user = self.local_user + with patch('bookwyrm.views.feed.is_api_request') as is_api: + is_api.return_value = False + result = view(request, 'mouse', status.id) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'feed/status.html') + self.assertEqual(result.status_code, 200) + + with patch('bookwyrm.views.feed.is_api_request') as is_api: + is_api.return_value = True + result = view(request, 'mouse', status.id) + self.assertIsInstance(result, ActivitypubResponse) + self.assertEqual(result.status_code, 200) + + + def test_direct_messages_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.DirectMessage.as_view() + request = self.factory.get('') + request.user = self.local_user + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'feed/direct_messages.html') + self.assertEqual(result.status_code, 200) + + + def test_get_suggested_book(self): + ''' gets books the ~*~ algorithm ~*~ thinks you want to post about ''' + models.ShelfBook.objects.create( + book=self.book, + added_by=self.local_user, + shelf=self.local_user.shelf_set.get(identifier='reading') + ) + suggestions = views.feed.get_suggested_books(self.local_user) + self.assertEqual(suggestions[0]['name'], 'Currently Reading') + self.assertEqual(suggestions[0]['books'][0], self.book) diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index 0f9c8988..8cac2b13 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -106,7 +106,7 @@ class PasswordViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'change_password.html') + self.assertEqual(result.template_name, 'preferences/change_password.html') self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_status.py b/bookwyrm/tests/views/test_status.py index b60feaa8..3be81c48 100644 --- a/bookwyrm/tests/views/test_status.py +++ b/bookwyrm/tests/views/test_status.py @@ -36,48 +36,6 @@ class StatusViews(TestCase): ) - def test_status_page(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.Status.as_view() - status = models.Status.objects.create( - content='hi', user=self.local_user) - request = self.factory.get('') - request.user = self.local_user - with patch('bookwyrm.views.status.is_api_request') as is_api: - is_api.return_value = False - result = view(request, 'mouse', status.id) - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/status.html') - self.assertEqual(result.status_code, 200) - - with patch('bookwyrm.views.status.is_api_request') as is_api: - is_api.return_value = True - result = view(request, 'mouse', status.id) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - - def test_replies_page(self): - ''' there are so many views, this just makes sure it LOADS ''' - view = views.Replies.as_view() - status = models.Status.objects.create( - content='hi', user=self.local_user) - request = self.factory.get('') - request.user = self.local_user - with patch('bookwyrm.views.status.is_api_request') as is_api: - is_api.return_value = False - result = view(request, 'mouse', status.id) - self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/status.html') - self.assertEqual(result.status_code, 200) - - with patch('bookwyrm.views.status.is_api_request') as is_api: - is_api.return_value = True - result = view(request, 'mouse', status.id) - self.assertIsInstance(result, ActivitypubResponse) - self.assertEqual(result.status_code, 200) - - def test_handle_status(self): ''' create a status ''' view = views.CreateStatus.as_view() From 779a5e0cbf09d568a33da27f05d7bd9e7d599295 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 11:14:18 -0800 Subject: [PATCH 035/280] Compose window for dms --- bookwyrm/forms.py | 6 +++++ bookwyrm/templates/feed/direct_messages.html | 22 ++++++++++++------- bookwyrm/templates/feed/feed_layout.html | 2 +- .../snippets/create_status_form.html | 11 +++++++--- bookwyrm/views/feed.py | 2 ++ 5 files changed, 31 insertions(+), 12 deletions(-) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 0e3ac9c1..e92bc180 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -92,6 +92,12 @@ class ReplyForm(CustomForm): 'user', 'content', 'content_warning', 'sensitive', 'reply_parent', 'privacy'] +class StatusForm(CustomForm): + class Meta: + model = models.Status + fields = [ + 'user', 'content', 'content_warning', 'sensitive', 'privacy'] + class EditUserForm(CustomForm): class Meta: diff --git a/bookwyrm/templates/feed/direct_messages.html b/bookwyrm/templates/feed/direct_messages.html index 44a0cded..1f41808f 100644 --- a/bookwyrm/templates/feed/direct_messages.html +++ b/bookwyrm/templates/feed/direct_messages.html @@ -4,16 +4,22 @@

    Direct Messages

    - {% if not activities %} -

    You have no messages right now.

    - {% endif %} - {% for activity in activities %} -
    - {% include 'snippets/status.html' with status=activity %} +
    + {% include 'snippets/create_status_form.html' with type="direct" uuid=1 %}
    - {% endfor %} - {% include 'snippets/pagination.html' with page=activities path="direct-messages" %} +
    + {% if not activities %} +

    You have no messages right now.

    + {% endif %} + {% for activity in activities %} +
    + {% include 'snippets/status.html' with status=activity %} +
    + {% endfor %} + + {% include 'snippets/pagination.html' with page=activities path="direct-messages" %} +
    {% endblock %} diff --git a/bookwyrm/templates/feed/feed_layout.html b/bookwyrm/templates/feed/feed_layout.html index 33123ca8..f0c7b057 100644 --- a/bookwyrm/templates/feed/feed_layout.html +++ b/bookwyrm/templates/feed/feed_layout.html @@ -76,7 +76,7 @@ {% block panel %}{% endblock %} {% if activities %} - {% include 'snippets/pagination.html' with page=activities path='/'|add:tab anchor="#feed" %} + {% include 'snippets/pagination.html' with page=activities path=path anchor="#feed" %} {% endif %}
    diff --git a/bookwyrm/templates/snippets/create_status_form.html b/bookwyrm/templates/snippets/create_status_form.html index 0c2ebbee..534ace29 100644 --- a/bookwyrm/templates/snippets/create_status_form.html +++ b/bookwyrm/templates/snippets/create_status_form.html @@ -1,5 +1,5 @@ {% load bookwyrm_tags %} -
    + {% csrf_token %} @@ -11,7 +11,7 @@
    {% endif %}
    - {% if not type == 'reply' %} + {% if type != 'reply' and type != 'direct' %} {% endif %} @@ -53,7 +53,12 @@ {% include 'snippets/toggle/toggle_button.html' with text="Include spoiler alert" icon="warning is-size-4" controls_text="spoilers" controls_uid=uuid focus="id_content_warning" checkbox="id_show_spoilers" class="toggle-button" pressed=status.content_warning %}
    - {% include 'snippets/privacy_select.html' with current=reply_parent.privacy%} + {% if type == 'direct' %} + + + {% else %} + {% include 'snippets/privacy_select.html' with current=reply_parent.privacy %} + {% endif %}
    - {% if not is_self %} + {% if not is_self and request.user.is_authenticated %}
    {% include 'snippets/follow_button.html' with user=user %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index d232747f..6d759ac2 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -62,6 +62,8 @@ urlpatterns = [ # feeds re_path(r'^(?Phome|local|federated)/?$', views.Feed.as_view()), re_path(r'^direct-messages/?$', views.DirectMessage.as_view()), + re_path(r'^direct-messages/(?P%s)?$' % regex.username, + views.DirectMessage.as_view()), # search re_path(r'^search/?$', views.Search.as_view()), diff --git a/bookwyrm/views/feed.py b/bookwyrm/views/feed.py index 08a5e057..0e550f0c 100644 --- a/bookwyrm/views/feed.py +++ b/bookwyrm/views/feed.py @@ -1,6 +1,7 @@ ''' non-interactive pages ''' from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator +from django.db.models import Q from django.http import HttpResponseNotFound from django.template.response import TemplateResponse from django.utils import timezone @@ -52,19 +53,33 @@ class Feed(View): @method_decorator(login_required, name='dispatch') class DirectMessage(View): ''' dm view ''' - def get(self, request): + def get(self, request, username=None): ''' like a feed but for dms only ''' try: page = int(request.GET.get('page', 1)) except ValueError: page = 1 - activities = get_activity_feed(request.user, 'direct') + queryset = models.Status.objects + + user = None + if username: + try: + user = get_user_from_username(username) + except models.User.DoesNotExist: + pass + if user: + queryset = queryset.filter(Q(user=user) | Q(mention_users=user)) + + activities = get_activity_feed( + request.user, 'direct', queryset=queryset) + paginated = Paginator(activities, PAGE_LENGTH) activity_page = paginated.page(page) data = {**feed_page_data(request.user), **{ 'title': 'Direct Messages', 'user': request.user, + 'partner': user, 'activities': activity_page, 'path': '/direct-messages', }} From 21aadf2920af349c672b796b70a71148cfb3a5e9 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 15:38:42 -0800 Subject: [PATCH 037/280] Adds site settings view --- bookwyrm/forms.py | 7 ++ bookwyrm/templates/layout.html | 2 +- bookwyrm/templates/settings/admin_layout.html | 42 ++++++++++ .../{ => settings}/manage_invites.html | 16 ++-- bookwyrm/templates/settings/site.html | 84 +++++++++++++++++++ bookwyrm/tests/views/test_invite.py | 2 +- bookwyrm/urls.py | 6 +- bookwyrm/views/__init__.py | 1 + bookwyrm/views/invite.py | 4 +- bookwyrm/views/site.py | 31 +++++++ 10 files changed, 182 insertions(+), 13 deletions(-) create mode 100644 bookwyrm/templates/settings/admin_layout.html rename bookwyrm/templates/{ => settings}/manage_invites.html (83%) create mode 100644 bookwyrm/templates/settings/site.html create mode 100644 bookwyrm/views/site.py diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index e92bc180..c8212794 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -195,7 +195,14 @@ class ShelfForm(CustomForm): model = models.Shelf fields = ['user', 'name', 'privacy'] + class GoalForm(CustomForm): class Meta: model = models.AnnualGoal fields = ['user', 'year', 'goal', 'privacy'] + + +class SiteForm(CustomForm): + class Meta: + model = models.SiteSettings + exclude = [] diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index a696e287..3a33196c 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -92,7 +92,7 @@ {% if perms.bookwyrm.create_invites %}
  • - + Invites
  • diff --git a/bookwyrm/templates/settings/admin_layout.html b/bookwyrm/templates/settings/admin_layout.html new file mode 100644 index 00000000..f66da658 --- /dev/null +++ b/bookwyrm/templates/settings/admin_layout.html @@ -0,0 +1,42 @@ +{% extends 'layout.html' %} +{% block content %} + +
    +

    {% block header %}{% endblock %}

    +
    + +
    + +
    + {% block panel %}{% endblock %} +
    +
    + +{% endblock %} diff --git a/bookwyrm/templates/manage_invites.html b/bookwyrm/templates/settings/manage_invites.html similarity index 83% rename from bookwyrm/templates/manage_invites.html rename to bookwyrm/templates/settings/manage_invites.html index a29ee5e4..03b68b20 100644 --- a/bookwyrm/templates/manage_invites.html +++ b/bookwyrm/templates/settings/manage_invites.html @@ -1,8 +1,8 @@ -{% extends 'layout.html' %} +{% extends 'settings/admin_layout.html' %} +{% block header %}Invites{% endblock %} {% load humanize %} -{% block content %} -
    -

    Invites

    +{% block panel %} +
    @@ -22,12 +22,12 @@ {% endfor %}
    Link
    -
    + -
    +

    Generate New Invite

    -
    + {% csrf_token %}
    @@ -46,5 +46,5 @@ -
    +
    {% endblock %} diff --git a/bookwyrm/templates/settings/site.html b/bookwyrm/templates/settings/site.html new file mode 100644 index 00000000..830a7dc5 --- /dev/null +++ b/bookwyrm/templates/settings/site.html @@ -0,0 +1,84 @@ +{% extends 'settings/admin_layout.html' %} +{% block header %}Site Configuration{% endblock %} + +{% block panel %} + +
    + {% csrf_token %} +
    +

    Instance Info

    +
    + + {{ site_form.name }} +
    +
    + + {{ site_form.instance_tagline }} +
    +
    + + {{ site_form.instance_description }} +
    +
    + + {{ site_form.code_of_conduct }} +
    +
    + + + +
    +

    Images

    +
    +
    + + {{ site_form.logo }} +
    +
    + + {{ site_form.logo_small }} +
    +
    + + {{ site_form.favicon }} +
    +
    +
    + + + + + + + +
    +

    Registration

    +
    +
    +
    + + {{ site_form.registration_closed_text }} +
    +
    + +
    + +
    +
    +{% endblock %} diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index 57b7a34a..eaffba72 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -44,5 +44,5 @@ class InviteViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'manage_invites.html') + self.assertEqual(result.template_name, 'admin/manage_invites.html') self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 6d759ac2..e79324a3 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -49,8 +49,12 @@ urlpatterns = [ re_path(r'^password-reset/(?P[A-Za-z0-9]+)/?$', views.PasswordReset.as_view()), + # admin + re_path(r'^settings/site-settings', + views.Site.as_view(), name='settings-site'), # invites - re_path(r'^invite/?$', views.ManageInvites.as_view()), + re_path(r'^settings/invites/?$', + views.ManageInvites.as_view(), name='settings-invites'), re_path(r'^invite/(?P[A-Za-z0-9]+)/?$', views.Invite.as_view()), # landing pages diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 80520eb3..62c4110f 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -24,6 +24,7 @@ from .search import Search from .shelf import Shelf from .shelf import user_shelves_page, create_shelf, delete_shelf from .shelf import shelve, unshelve +from .site import Site from .status import CreateStatus, DeleteStatus from .updates import Updates from .user import User, EditUser, Followers, Following diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index 56485861..bd0715a0 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -24,7 +24,7 @@ class ManageInvites(View): user=request.user).order_by('-created_date'), 'form': forms.CreateInviteForm(), } - return TemplateResponse(request, 'manage_invites.html', data) + return TemplateResponse(request, 'settings/manage_invites.html', data) def post(self, request): ''' creates an invite database entry ''' @@ -36,7 +36,7 @@ class ManageInvites(View): invite.user = request.user invite.save() - return redirect('/invite') + return redirect('/settings/invites') class Invite(View): diff --git a/bookwyrm/views/site.py b/bookwyrm/views/site.py new file mode 100644 index 00000000..85c1751d --- /dev/null +++ b/bookwyrm/views/site.py @@ -0,0 +1,31 @@ +''' manage site settings ''' +from django.contrib.auth.decorators import login_required, permission_required +from django.shortcuts import redirect +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import forms, models + + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +@method_decorator( + permission_required( + 'bookwyrm.edit_instance_settings', raise_exception=True), + name='dispatch') +class Site(View): + ''' manage things like the instance name ''' + def get(self, request): + ''' edit form ''' + site = models.SiteSettings.objects.get() + data = { + 'title': 'Site Settings', + 'site_form': forms.SiteForm(instance=site) + } + return TemplateResponse(request, 'settings/site.html', data) + + def post(self, request): + ''' edit the site settings ''' + + return redirect('/settings/site-settings') From 9db327a69efb8734f22d5f4e22c50ed3702a40aa Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 15:50:46 -0800 Subject: [PATCH 038/280] Edit view for site settings --- bookwyrm/templates/layout.html | 10 ++++++++++ bookwyrm/templates/settings/site.html | 4 ++-- bookwyrm/views/site.py | 11 ++++++++++- 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 3a33196c..81782970 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -90,6 +90,9 @@ Import books + {% if perms.bookwyrm.create_invites or perms.bookwyrm.edit_instance_settings%} + + {% endif %} {% if perms.bookwyrm.create_invites %}
  • @@ -97,6 +100,13 @@
  • {% endif %} + {% if perms.bookwyrm.edit_instance_settings %} +
  • + + Site Configuration + +
  • + {% endif %}
  • diff --git a/bookwyrm/templates/settings/site.html b/bookwyrm/templates/settings/site.html index 830a7dc5..d071facf 100644 --- a/bookwyrm/templates/settings/site.html +++ b/bookwyrm/templates/settings/site.html @@ -51,11 +51,11 @@

    Footer Content

    - +
    - +
  • +
  • + {% url 'settings-federation' as url %} + Federated Servers +
  • {% endif %} {% if perms.bookwyrm.edit_instance_settings %} diff --git a/bookwyrm/templates/settings/federation.html b/bookwyrm/templates/settings/federation.html new file mode 100644 index 00000000..e3bf23ce --- /dev/null +++ b/bookwyrm/templates/settings/federation.html @@ -0,0 +1,21 @@ +{% extends 'settings/admin_layout.html' %} +{% block header %}Federated Servers{% endblock %} + +{% block panel %} + + + + + + + + {% for server in servers %} + + + + + + {% endfor %} +
    Server nameSoftwareStatus
    {{ server.server_name }}{{ server.application_type }} ({{ server.application_version }}){{ server.status }}
    + +{% endblock %} diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index e79324a3..6387a4cd 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -52,7 +52,8 @@ urlpatterns = [ # admin re_path(r'^settings/site-settings', views.Site.as_view(), name='settings-site'), - # invites + re_path(r'^settings/federation', + views.Federation.as_view(), name='settings-federation'), re_path(r'^settings/invites/?$', views.ManageInvites.as_view(), name='settings-invites'), re_path(r'^invite/(?P[A-Za-z0-9]+)/?$', views.Invite.as_view()), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 62c4110f..0b93f164 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -5,6 +5,7 @@ from .block import Block, unblock from .books import Book, EditBook, Editions from .books import upload_cover, add_description, switch_edition, resolve_book from .error import not_found_page, server_error_page +from .federation import Federation from .feed import DirectMessage, Feed, Replies, Status from .follow import follow, unfollow from .follow import accept_follow_request, delete_follow_request, handle_accept diff --git a/bookwyrm/views/federation.py b/bookwyrm/views/federation.py new file mode 100644 index 00000000..0bd14dab --- /dev/null +++ b/bookwyrm/views/federation.py @@ -0,0 +1,24 @@ +''' manage federated servers ''' +from django.contrib.auth.decorators import login_required, permission_required +from django.template.response import TemplateResponse +from django.utils.decorators import method_decorator +from django.views import View + +from bookwyrm import models + + +# pylint: disable= no-self-use +@method_decorator(login_required, name='dispatch') +@method_decorator( + permission_required('bookwyrm.control_federation', raise_exception=True), + name='dispatch') +class Federation(View): + ''' what servers do we federate with ''' + def get(self, request): + ''' edit form ''' + servers = models.FederatedServer.objects.all() + data = { + 'title': 'Federated Servers', + 'servers': servers + } + return TemplateResponse(request, 'settings/federation.html', data) From 9d3f48a1a9a3bd25fc7a4da2f7d83ff728893181 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 16:30:47 -0800 Subject: [PATCH 040/280] Settings views tests --- bookwyrm/tests/views/test_federation.py | 29 +++++++++++++++++++++++++ bookwyrm/tests/views/test_invite.py | 2 +- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 bookwyrm/tests/views/test_federation.py diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py new file mode 100644 index 00000000..2a182a21 --- /dev/null +++ b/bookwyrm/tests/views/test_federation.py @@ -0,0 +1,29 @@ +''' test for app action functionality ''' +from django.template.response import TemplateResponse +from django.test import TestCase +from django.test.client import RequestFactory + +from bookwyrm import models +from bookwyrm import views + + +class FederationViews(TestCase): + ''' every response to a get request, html or json ''' + def setUp(self): + ''' we need basic test data and mocks ''' + self.factory = RequestFactory() + self.local_user = models.User.objects.create_user( + 'mouse@local.com', 'mouse@mouse.mouse', 'password', + local=True, localname='mouse') + + + def test_federation_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Federation.as_view() + request = self.factory.get('') + request.user = self.local_user + request.user.is_superuser = True + result = view(request) + self.assertIsInstance(result, TemplateResponse) + self.assertEqual(result.template_name, 'settings/federation.html') + self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index eaffba72..85741688 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -44,5 +44,5 @@ class InviteViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'admin/manage_invites.html') + self.assertEqual(result.template_name, 'settings/manage_invites.html') self.assertEqual(result.status_code, 200) From 8add05d7fd4a00b9e02b1535c22cb8756da5ef72 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Fri, 29 Jan 2021 18:06:17 -0800 Subject: [PATCH 041/280] Fixes incorrect display of reading progress --- bookwyrm/templates/snippets/readthrough.html | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/readthrough.html b/bookwyrm/templates/snippets/readthrough.html index e0b25d8b..7e51ce97 100644 --- a/bookwyrm/templates/snippets/readthrough.html +++ b/bookwyrm/templates/snippets/readthrough.html @@ -6,10 +6,11 @@ Progress Updates:
      - {% if readthrough.progress %} + {% if readthrough.finish_date or readthrough.progress %}
    • {% if readthrough.finish_date %} {{ readthrough.finish_date | naturalday }}: finished {% else %}{% if readthrough.progress_mode == 'PG' %}on page {{ readthrough.progress }}{% if book.pages %} of {{ book.pages }}{% endif %} {% else %}{{ readthrough.progress }}%{% endif %}{% endif %} - {% include 'snippets/toggle/toggle_button.html' with text="Show all updates" controls_text="updates" controls_uid=readthrough.id class="is-small" %} + {% if readthrough.progress %} + {% include 'snippets/toggle/toggle_button.html' with text="Show all updates" controls_text="updates" controls_uid=readthrough.id class="is-small" %} + {% endif %}
    • {% endif %}
    • {{ readthrough.start_date | naturalday }}: started
    • From e5746d611783c2410c6e33dadd157798a9e322ad Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 11:43:40 -0800 Subject: [PATCH 042/280] Menu list for shelf button options --- bookwyrm/static/css/format.css | 7 +++++++ .../templates/snippets/shelve_button.html | 17 +--------------- .../snippets/shelve_button_dropdown.html | 20 +------------------ .../snippets/toggle/toggle_button.html | 3 ++- bookwyrm/templatetags/bookwyrm_tags.py | 13 ++++++++++++ 5 files changed, 24 insertions(+), 36 deletions(-) diff --git a/bookwyrm/static/css/format.css b/bookwyrm/static/css/format.css index f10549ed..50ce101e 100644 --- a/bookwyrm/static/css/format.css +++ b/bookwyrm/static/css/format.css @@ -13,6 +13,13 @@ overflow: hidden; } +/* --- SHELVING --- */ +.shelf-option:disabled > *::after { + font-family: "icomoon"; + content: "\e918"; + margin-left: 0.5em; +} + /* --- TOGGLES --- */ .toggle-button[aria-pressed=true], .toggle-button[aria-pressed=true]:hover { background-color: hsl(171, 100%, 41%); diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index f69ab525..946368c2 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -10,22 +10,7 @@
    {% else %}
    - {% if active_shelf.shelf.identifier == 'read' %} - - {% elif active_shelf.shelf.identifier == 'reading' %} - {% include 'snippets/toggle/toggle_button.html' with class="is-small" text="I'm done!" controls_text="finish-reading" controls_uid=uuid focus="modal-title-finish-reading" %} - {% elif active_shelf.shelf.identifier == 'to-read' %} - {% include 'snippets/toggle/toggle_button.html' with class="is-small" text="Start reading" controls_text="start-reading" controls_uid=uuid focus="modal-title-start-reading" %} - {% else %} -
    - {% csrf_token %} - - - -
    - {% endif %} + {% include 'snippets/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf %}
    {% include 'snippets/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%} {% endif %} diff --git a/bookwyrm/templates/snippets/shelve_button_dropdown.html b/bookwyrm/templates/snippets/shelve_button_dropdown.html index 50140e3f..d70765a3 100644 --- a/bookwyrm/templates/snippets/shelve_button_dropdown.html +++ b/bookwyrm/templates/snippets/shelve_button_dropdown.html @@ -6,23 +6,5 @@ {% endblock %} {% block dropdown-list %} -{% for shelf in request.user.shelf_set.all %} -
  • - {% if active_shelf.shelf.identifier != 'reading' and shelf.identifier == 'reading' %} - - {% else %} - - {% endif %} -
  • -{% endfor %} +{% include 'snippets/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %} {% endblock %} - diff --git a/bookwyrm/templates/snippets/toggle/toggle_button.html b/bookwyrm/templates/snippets/toggle/toggle_button.html index d6fd0a36..fe1823f1 100644 --- a/bookwyrm/templates/snippets/toggle/toggle_button.html +++ b/bookwyrm/templates/snippets/toggle/toggle_button.html @@ -6,6 +6,7 @@ {% if checkbox %}data-controls-checkbox="{{ checkbox }}{% if controls_uid %}-{{ controls_uid }}{% endif %}"{% endif %} {% if label %}aria-label="{{ label }}"{% endif %} aria-pressed="{% if pressed %}true{% else %}false{% endif %}" + {% if disabled %}disabled{% endif %} > {% if icon %} @@ -13,6 +14,6 @@ {{ text }} {% else %} - {{ text }} + {{ text }} {% endif %} diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index 08bbcafe..c1a2d14e 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -159,6 +159,14 @@ def get_status_preview_name(obj): return '%s from %s' % (name, obj.book.title) return name +@register.filter(name='next_shelf') +def get_next_shelf(current_shelf): + if current_shelf == 'to-read': + return 'reading' + if current_shelf == 'reading': + return 'read' + return 'to-read' + @register.simple_tag(takes_context=False) def related_status(notification): ''' for notifications ''' @@ -199,3 +207,8 @@ def active_read_through(book, user): book=book, finish_date__isnull=True ).order_by('-start_date').first() + + +@register.simple_tag(takes_context=False) +def comparison_bool(str1, str2): + return str1 == str2 From cd65ac72902470ca0bdddfe7c2cdc9228c044488 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 11:46:15 -0800 Subject: [PATCH 043/280] Fixes incorrect tempalte path in threaded status page --- bookwyrm/templates/feed/thread.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/feed/thread.html b/bookwyrm/templates/feed/thread.html index 589702ce..aa67d5bb 100644 --- a/bookwyrm/templates/feed/thread.html +++ b/bookwyrm/templates/feed/thread.html @@ -4,7 +4,7 @@ {% with depth=depth|add:1 %} {% if depth <= max_depth and status.reply_parent and direction <= 0 %} {% with direction=-1 %} - {% include 'snippets/thread.html' with status=status|parent is_root=False %} + {% include 'feed/thread.html' with status=status|parent is_root=False %} {% endwith %} {% endif %} @@ -13,7 +13,7 @@ {% if depth <= max_depth and direction >= 0 %} {% for reply in status|replies %} {% with direction=1 %} - {% include 'snippets/thread.html' with status=reply is_root=False %} + {% include 'feed/thread.html' with status=reply is_root=False %} {% endwith %} {% endfor %} {% endif %} From 37710144fa9919cdbb30224090d543afdba61eba Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 11:52:31 -0800 Subject: [PATCH 044/280] Adds options tempalte --- .../snippets/shelve_button_options.html | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 bookwyrm/templates/snippets/shelve_button_options.html diff --git a/bookwyrm/templates/snippets/shelve_button_options.html b/bookwyrm/templates/snippets/shelve_button_options.html new file mode 100644 index 00000000..84133400 --- /dev/null +++ b/bookwyrm/templates/snippets/shelve_button_options.html @@ -0,0 +1,23 @@ +{% load bookwyrm_tags %} +{% for shelf in shelves %} +{% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} +{% if dropdown %}
  • {% endif %} + +{% if dropdown %}
  • {% endif %} +{% endfor %} From ac4a178e8319e45388b9e1e0afefb67f5c8b749f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 12:03:50 -0800 Subject: [PATCH 045/280] Fixes shelf page template path bug --- bookwyrm/templates/{ => user}/shelf.html | 28 +++++++++++------------- bookwyrm/tests/views/test_shelf.py | 2 +- bookwyrm/views/shelf.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) rename bookwyrm/templates/{ => user}/shelf.html (89%) diff --git a/bookwyrm/templates/shelf.html b/bookwyrm/templates/user/shelf.html similarity index 89% rename from bookwyrm/templates/shelf.html rename to bookwyrm/templates/user/shelf.html index da599c7d..c0083e17 100644 --- a/bookwyrm/templates/shelf.html +++ b/bookwyrm/templates/user/shelf.html @@ -1,21 +1,19 @@ -{% extends 'layout.html' %} +{% extends 'user/user_layout.html' %} {% load bookwyrm_tags %} -{% block content %} -
    -
    -

    - {% if is_self %}Your - {% else %} - {% include 'snippets/username.html' with user=user possessive=True %} - {% endif %} - shelves -

    -
    -
    - -{% include 'snippets/user_header.html' with user=user %} +{% block header %} +
    +

    + {% if is_self %}Your + {% else %} + {% include 'snippets/username.html' with user=user possessive=True %} + {% endif %} + shelves +

    +
    +{% endblock %} +{% block panel %}
    diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 35e07953..95150fe9 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -41,7 +41,7 @@ class ShelfViews(TestCase): is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'shelf.html') + self.assertEqual(result.template_name, 'user/shelf.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.shelf.is_api_request') as is_api: diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index ae8a494b..e4c49261 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -62,7 +62,7 @@ class Shelf(View): 'books': [b.book for b in books], } - return TemplateResponse(request, 'shelf.html', data) + return TemplateResponse(request, 'user/shelf.html', data) @method_decorator(login_required, name='dispatch') # pylint: disable=unused-argument From b3bd6822b20423ed018a1d3b9d675d348e08132d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 12:16:57 -0800 Subject: [PATCH 046/280] Make sure templates render rather than checking template paths --- bookwyrm/tests/views/test_authentication.py | 10 +++++----- bookwyrm/tests/views/test_author.py | 10 +++++++--- bookwyrm/tests/views/test_block.py | 3 ++- bookwyrm/tests/views/test_book.py | 7 ++++--- bookwyrm/tests/views/test_federation.py | 3 ++- bookwyrm/tests/views/test_feed.py | 9 +++++---- bookwyrm/tests/views/test_import.py | 5 +++-- bookwyrm/tests/views/test_invite.py | 5 +++-- bookwyrm/tests/views/test_landing.py | 9 ++++----- bookwyrm/tests/views/test_notifications.py | 3 ++- bookwyrm/tests/views/test_password.py | 13 +++++++------ bookwyrm/tests/views/test_search.py | 5 +++-- bookwyrm/tests/views/test_shelf.py | 3 ++- bookwyrm/tests/views/test_tag.py | 3 ++- bookwyrm/tests/views/test_user.py | 9 +++++---- 15 files changed, 56 insertions(+), 41 deletions(-) diff --git a/bookwyrm/tests/views/test_authentication.py b/bookwyrm/tests/views/test_authentication.py index 65577208..dc52719c 100644 --- a/bookwyrm/tests/views/test_authentication.py +++ b/bookwyrm/tests/views/test_authentication.py @@ -33,7 +33,7 @@ class AuthenticationViews(TestCase): result = login(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'login.html') + result.render() self.assertEqual(result.status_code, 200) request.user = self.local_user @@ -94,7 +94,7 @@ class AuthenticationViews(TestCase): }) response = view(request) self.assertEqual(models.User.objects.count(), 1) - self.assertEqual(response.template_name, 'login.html') + response.render() def test_register_invalid_username(self): ''' gotta have an email ''' @@ -109,7 +109,7 @@ class AuthenticationViews(TestCase): }) response = view(request) self.assertEqual(models.User.objects.count(), 1) - self.assertEqual(response.template_name, 'login.html') + response.render() request = self.factory.post( 'register/', @@ -120,7 +120,7 @@ class AuthenticationViews(TestCase): }) response = view(request) self.assertEqual(models.User.objects.count(), 1) - self.assertEqual(response.template_name, 'login.html') + response.render() request = self.factory.post( 'register/', @@ -131,7 +131,7 @@ class AuthenticationViews(TestCase): }) response = view(request) self.assertEqual(models.User.objects.count(), 1) - self.assertEqual(response.template_name, 'login.html') + response.render() def test_register_closed_instance(self): diff --git a/bookwyrm/tests/views/test_author.py b/bookwyrm/tests/views/test_author.py index c00972f3..c92c4750 100644 --- a/bookwyrm/tests/views/test_author.py +++ b/bookwyrm/tests/views/test_author.py @@ -34,6 +34,7 @@ class AuthorViews(TestCase): remote_id='https://example.com/book/1', parent_work=self.work ) + models.SiteSettings.objects.create() def test_author_page(self): @@ -45,7 +46,8 @@ class AuthorViews(TestCase): is_api.return_value = False result = view(request, author.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'author.html') + result.render() + self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200) request = self.factory.get('') @@ -66,7 +68,8 @@ class AuthorViews(TestCase): result = view(request, author.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'edit_author.html') + result.render() + self.assertEqual(result.status_code, 200) self.assertEqual(result.status_code, 200) @@ -116,4 +119,5 @@ class AuthorViews(TestCase): resp = view(request, author.id) author.refresh_from_db() self.assertEqual(author.name, 'Test Author') - self.assertEqual(resp.template_name, 'edit_author.html') + resp.render() + self.assertEqual(resp.status_code, 200) diff --git a/bookwyrm/tests/views/test_block.py b/bookwyrm/tests/views/test_block.py index f3a0c2f8..6f85f282 100644 --- a/bookwyrm/tests/views/test_block.py +++ b/bookwyrm/tests/views/test_block.py @@ -23,6 +23,7 @@ class BlockViews(TestCase): inbox='https://example.com/users/rat/inbox', outbox='https://example.com/users/rat/outbox', ) + models.SiteSettings.objects.create() def test_block_get(self): @@ -32,7 +33,7 @@ class BlockViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'preferences/blocks.html') + result.render() self.assertEqual(result.status_code, 200) def test_block_post(self): diff --git a/bookwyrm/tests/views/test_book.py b/bookwyrm/tests/views/test_book.py index 8306b803..32a407d6 100644 --- a/bookwyrm/tests/views/test_book.py +++ b/bookwyrm/tests/views/test_book.py @@ -33,6 +33,7 @@ class BookViews(TestCase): remote_id='https://example.com/book/1', parent_work=self.work ) + models.SiteSettings.objects.create() def test_book_page(self): @@ -44,7 +45,7 @@ class BookViews(TestCase): is_api.return_value = False result = view(request, self.book.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'book.html') + result.render() self.assertEqual(result.status_code, 200) request = self.factory.get('') @@ -63,7 +64,7 @@ class BookViews(TestCase): request.user.is_superuser = True result = view(request, self.book.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'edit_book.html') + result.render() self.assertEqual(result.status_code, 200) @@ -116,7 +117,7 @@ class BookViews(TestCase): is_api.return_value = False result = view(request, self.work.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'editions.html') + result.render() self.assertEqual(result.status_code, 200) request = self.factory.get('') diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index 2a182a21..70cf41f6 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -15,6 +15,7 @@ class FederationViews(TestCase): self.local_user = models.User.objects.create_user( 'mouse@local.com', 'mouse@mouse.mouse', 'password', local=True, localname='mouse') + models.SiteSettings.objects.create() def test_federation_page(self): @@ -25,5 +26,5 @@ class FederationViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'settings/federation.html') + result.render() self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_feed.py b/bookwyrm/tests/views/test_feed.py index da597a7c..879dd2b6 100644 --- a/bookwyrm/tests/views/test_feed.py +++ b/bookwyrm/tests/views/test_feed.py @@ -21,6 +21,7 @@ class FeedMessageViews(TestCase): title='Example Edition', remote_id='https://example.com/book/1', ) + models.SiteSettings.objects.create() def test_feed(self): @@ -30,7 +31,7 @@ class FeedMessageViews(TestCase): request.user = self.local_user result = view(request, 'local') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/feed.html') + result.render() self.assertEqual(result.status_code, 200) @@ -45,7 +46,7 @@ class FeedMessageViews(TestCase): is_api.return_value = False result = view(request, 'mouse', status.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/status.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.feed.is_api_request') as is_api: @@ -66,7 +67,7 @@ class FeedMessageViews(TestCase): is_api.return_value = False result = view(request, 'mouse', status.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/status.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.feed.is_api_request') as is_api: @@ -83,7 +84,7 @@ class FeedMessageViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'feed/direct_messages.html') + result.render() self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_import.py b/bookwyrm/tests/views/test_import.py index 14209f24..ba8f2457 100644 --- a/bookwyrm/tests/views/test_import.py +++ b/bookwyrm/tests/views/test_import.py @@ -16,6 +16,7 @@ class ImportViews(TestCase): self.local_user = models.User.objects.create_user( 'mouse@local.com', 'mouse@mouse.mouse', 'password', local=True, localname='mouse') + models.SiteSettings.objects.create() def test_import_page(self): @@ -25,7 +26,7 @@ class ImportViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'import.html') + result.render() self.assertEqual(result.status_code, 200) @@ -39,5 +40,5 @@ class ImportViews(TestCase): async_result.return_value = [] result = view(request, import_job.id) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'import_status.html') + result.render() self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_invite.py b/bookwyrm/tests/views/test_invite.py index 85741688..e93e7209 100644 --- a/bookwyrm/tests/views/test_invite.py +++ b/bookwyrm/tests/views/test_invite.py @@ -18,6 +18,7 @@ class InviteViews(TestCase): self.local_user = models.User.objects.create_user( 'mouse@local.com', 'mouse@mouse.mouse', 'password', local=True, localname='mouse') + models.SiteSettings.objects.create() def test_invite_page(self): @@ -32,7 +33,7 @@ class InviteViews(TestCase): invite.return_value = True result = view(request, 'hi') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'invite.html') + result.render() self.assertEqual(result.status_code, 200) @@ -44,5 +45,5 @@ class InviteViews(TestCase): request.user.is_superuser = True result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'settings/manage_invites.html') + result.render() self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_landing.py b/bookwyrm/tests/views/test_landing.py index 8576af49..5e0e50cf 100644 --- a/bookwyrm/tests/views/test_landing.py +++ b/bookwyrm/tests/views/test_landing.py @@ -18,6 +18,7 @@ class LandingViews(TestCase): local=True, localname='mouse') self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False + models.SiteSettings.objects.create() def test_home_page(self): @@ -27,13 +28,13 @@ class LandingViews(TestCase): request.user = self.local_user result = view(request) self.assertEqual(result.status_code, 200) - self.assertEqual(result.template_name, 'feed/feed.html') + result.render() request.user = self.anonymous_user result = view(request) self.assertIsInstance(result, TemplateResponse) self.assertEqual(result.status_code, 200) - self.assertEqual(result.template_name, 'discover.html') + result.render() def test_about_page(self): @@ -43,7 +44,7 @@ class LandingViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'about.html') + result.render() self.assertEqual(result.status_code, 200) @@ -53,5 +54,3 @@ class LandingViews(TestCase): request = self.factory.get('') result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'discover.html') - self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_notifications.py b/bookwyrm/tests/views/test_notifications.py index 683424d5..24fbde1e 100644 --- a/bookwyrm/tests/views/test_notifications.py +++ b/bookwyrm/tests/views/test_notifications.py @@ -15,6 +15,7 @@ class NotificationViews(TestCase): self.local_user = models.User.objects.create_user( 'mouse@local.com', 'mouse@mouse.mouse', 'password', local=True, localname='mouse') + models.SiteSettings.objects.create() def test_notifications_page(self): ''' there are so many views, this just makes sure it LOADS ''' @@ -23,7 +24,7 @@ class NotificationViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'notifications.html') + result.render() self.assertEqual(result.status_code, 200) def test_clear_notifications(self): diff --git a/bookwyrm/tests/views/test_password.py b/bookwyrm/tests/views/test_password.py index 8cac2b13..9fc37fdb 100644 --- a/bookwyrm/tests/views/test_password.py +++ b/bookwyrm/tests/views/test_password.py @@ -19,6 +19,7 @@ class PasswordViews(TestCase): local=True, localname='mouse') self.anonymous_user = AnonymousUser self.anonymous_user.is_authenticated = False + models.SiteSettings.objects.create(id=1) def test_password_reset_request(self): @@ -29,7 +30,7 @@ class PasswordViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'password_reset_request.html') + result.render() self.assertEqual(result.status_code, 200) @@ -43,7 +44,7 @@ class PasswordViews(TestCase): request = self.factory.post('', {'email': 'mouse@mouse.com'}) with patch('bookwyrm.emailing.send_email.delay'): resp = view(request) - self.assertEqual(resp.template_name, 'password_reset_request.html') + resp.render() self.assertEqual( models.PasswordReset.objects.get().user, self.local_user) @@ -56,7 +57,7 @@ class PasswordViews(TestCase): request.user = self.anonymous_user result = view(request, code.code) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'password_reset.html') + result.render() self.assertEqual(result.status_code, 200) @@ -82,7 +83,7 @@ class PasswordViews(TestCase): 'confirm-password': 'hi' }) resp = view(request, 'jhgdkfjgdf') - self.assertEqual(resp.template_name, 'password_reset.html') + resp.render() self.assertTrue(models.PasswordReset.objects.exists()) def test_password_reset_mismatch(self): @@ -94,7 +95,7 @@ class PasswordViews(TestCase): 'confirm-password': 'hihi' }) resp = view(request, code.code) - self.assertEqual(resp.template_name, 'password_reset.html') + resp.render() self.assertTrue(models.PasswordReset.objects.exists()) @@ -106,7 +107,7 @@ class PasswordViews(TestCase): result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'preferences/change_password.html') + result.render() self.assertEqual(result.status_code, 200) diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index 3f1d7850..e5cba99c 100644 --- a/bookwyrm/tests/views/test_search.py +++ b/bookwyrm/tests/views/test_search.py @@ -33,6 +33,7 @@ class ShelfViews(TestCase): connector_file='self_connector', local=True ) + models.SiteSettings.objects.create() def test_search_json_response(self): @@ -89,7 +90,7 @@ class ShelfViews(TestCase): manager.return_value = [search_result] response = view(request) self.assertIsInstance(response, TemplateResponse) - self.assertEqual(response.template_name, 'search_results.html') + response.render() self.assertEqual( response.context_data['book_results'][0].title, 'Gideon the Ninth') @@ -103,6 +104,6 @@ class ShelfViews(TestCase): with patch('bookwyrm.connectors.connector_manager.search'): response = view(request) self.assertIsInstance(response, TemplateResponse) - self.assertEqual(response.template_name, 'search_results.html') + response.render() self.assertEqual( response.context_data['user_results'][0], self.local_user) diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 95150fe9..f67bbc56 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -29,6 +29,7 @@ class ShelfViews(TestCase): identifier='test-shelf', user=self.local_user ) + models.SiteSettings.objects.create() def test_shelf_page(self): @@ -41,7 +42,7 @@ class ShelfViews(TestCase): is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'user/shelf.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.shelf.is_api_request') as is_api: diff --git a/bookwyrm/tests/views/test_tag.py b/bookwyrm/tests/views/test_tag.py index 1556139c..3dfef9a1 100644 --- a/bookwyrm/tests/views/test_tag.py +++ b/bookwyrm/tests/views/test_tag.py @@ -33,6 +33,7 @@ class TagViews(TestCase): remote_id='https://example.com/book/1', parent_work=self.work ) + models.SiteSettings.objects.create() def test_tag_page(self): @@ -46,7 +47,7 @@ class TagViews(TestCase): is_api.return_value = False result = view(request, tag.identifier) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'tag.html') + result.render() self.assertEqual(result.status_code, 200) request = self.factory.get('') diff --git a/bookwyrm/tests/views/test_user.py b/bookwyrm/tests/views/test_user.py index 61fcdb64..616a6575 100644 --- a/bookwyrm/tests/views/test_user.py +++ b/bookwyrm/tests/views/test_user.py @@ -23,6 +23,7 @@ class UserViews(TestCase): self.rat = models.User.objects.create_user( 'rat@local.com', 'rat@rat.rat', 'password', local=True, localname='rat') + models.SiteSettings.objects.create() def test_user_page(self): @@ -34,7 +35,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'user/user.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -65,7 +66,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'user/followers.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -96,7 +97,7 @@ class UserViews(TestCase): is_api.return_value = False result = view(request, 'mouse') self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'user/following.html') + result.render() self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.user.is_api_request') as is_api: @@ -125,7 +126,7 @@ class UserViews(TestCase): request.user = self.local_user result = view(request) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'preferences/edit_user.html') + result.render() self.assertEqual(result.status_code, 200) From e5fd14b2067b54a095bd374cdccbbb215ca8b52e Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 13:11:43 -0800 Subject: [PATCH 047/280] Confirmation modal for to-read shelve action --- bookwyrm/templates/components/modal.html | 4 ++- .../templates/snippets/shelve_button.html | 4 ++- .../snippets/shelve_button_options.html | 4 ++- .../snippets/want_to_read_modal.html | 30 +++++++++++++++++++ bookwyrm/templatetags/bookwyrm_tags.py | 4 +++ 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 bookwyrm/templates/snippets/want_to_read_modal.html diff --git a/bookwyrm/templates/components/modal.html b/bookwyrm/templates/components/modal.html index 72402914..554f9ccd 100644 --- a/bookwyrm/templates/components/modal.html +++ b/bookwyrm/templates/components/modal.html @@ -8,15 +8,17 @@ {% include 'snippets/toggle/toggle_button.html' with label="close" class="delete" nonbutton=True %} {% block modal-form-open %}{% endblock %} + {% if not no_body %} + {% endif %}
    {% block modal-footer %}{% endblock %}
    {% block modal-form-close %}{% endblock %}
    - + {% include 'snippets/toggle/toggle_button.html' with label="close" class="modal-close is-large" nonbutton=True %}
    diff --git a/bookwyrm/templates/snippets/shelve_button.html b/bookwyrm/templates/snippets/shelve_button.html index 946368c2..769da50f 100644 --- a/bookwyrm/templates/snippets/shelve_button.html +++ b/bookwyrm/templates/snippets/shelve_button.html @@ -10,12 +10,14 @@
    {% else %}
    - {% include 'snippets/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf %} + {% include 'snippets/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %}
    {% include 'snippets/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%} {% endif %}
    +{% include 'snippets/want_to_read_modal.html' with book=active_shelf.book controls_text="want-to-read" controls_uid=uuid no_body=True %} + {% include 'snippets/start_reading_modal.html' with book=active_shelf.book controls_text="start-reading" controls_uid=uuid %} {% latest_read_through book request.user as readthrough %} diff --git a/bookwyrm/templates/snippets/shelve_button_options.html b/bookwyrm/templates/snippets/shelve_button_options.html index 84133400..af47ee23 100644 --- a/bookwyrm/templates/snippets/shelve_button_options.html +++ b/bookwyrm/templates/snippets/shelve_button_options.html @@ -5,10 +5,12 @@ +
    +{% endblock %} +{% block modal-form-close %}{% endblock %} diff --git a/bookwyrm/templatetags/bookwyrm_tags.py b/bookwyrm/templatetags/bookwyrm_tags.py index c1a2d14e..0a699129 100644 --- a/bookwyrm/templatetags/bookwyrm_tags.py +++ b/bookwyrm/templatetags/bookwyrm_tags.py @@ -161,10 +161,13 @@ def get_status_preview_name(obj): @register.filter(name='next_shelf') def get_next_shelf(current_shelf): + ''' shelf you'd use to update reading progress ''' if current_shelf == 'to-read': return 'reading' if current_shelf == 'reading': return 'read' + if current_shelf == 'read': + return 'read' return 'to-read' @register.simple_tag(takes_context=False) @@ -211,4 +214,5 @@ def active_read_through(book, user): @register.simple_tag(takes_context=False) def comparison_bool(str1, str2): + ''' idk why I need to write a tag for this, it reutrns a bool ''' return str1 == str2 From 9b326f732158333f9dcb713e2b759ddc2babe7d7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 15:18:38 -0800 Subject: [PATCH 048/280] Give user control over "want to read" posts --- bookwyrm/templates/snippets/want_to_read_modal.html | 3 ++- bookwyrm/views/shelf.py | 8 ++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/bookwyrm/templates/snippets/want_to_read_modal.html b/bookwyrm/templates/snippets/want_to_read_modal.html index 37a4c39a..9b7e2bde 100644 --- a/bookwyrm/templates/snippets/want_to_read_modal.html +++ b/bookwyrm/templates/snippets/want_to_read_modal.html @@ -8,6 +8,7 @@ Want to Read "{{ book.title }}"
    {% csrf_token %} + {% endblock %} {% block modal-footer %} @@ -20,7 +21,7 @@ Want to Read "{{ book.title }}" {% include 'snippets/privacy_select.html' %}
    - {% include 'snippets/toggle/toggle_button.html' with text="Cancel" controls_text="want-to-read" controls_uid=uuid %} diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index ae8a494b..25275d67 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -125,6 +125,8 @@ def shelve(request): identifier=request.POST.get('shelf'), user=request.user ).first() + #if not desired_shelf: + # return HttpResponseNotFound() if request.POST.get('reshelve', True): try: @@ -141,12 +143,14 @@ def shelve(request): broadcast(request.user, shelfbook.to_add_activity(request.user)) # post about "want to read" shelves - if desired_shelf.identifier == 'to-read': + if desired_shelf.identifier == 'to-read' and \ + request.POST.get('post-status'): + privacy = request.POST.get('privacy') or desired_shelf.privacy handle_reading_status( request.user, desired_shelf, book, - privacy=desired_shelf.privacy + privacy=privacy ) return redirect('/') From 7afab352b1585ceaa55258ebf5c8cb5f41dcf5a1 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 11:46:15 -0800 Subject: [PATCH 049/280] Fixes incorrect tempalte path in threaded status page --- bookwyrm/templates/feed/thread.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/feed/thread.html b/bookwyrm/templates/feed/thread.html index 589702ce..aa67d5bb 100644 --- a/bookwyrm/templates/feed/thread.html +++ b/bookwyrm/templates/feed/thread.html @@ -4,7 +4,7 @@ {% with depth=depth|add:1 %} {% if depth <= max_depth and status.reply_parent and direction <= 0 %} {% with direction=-1 %} - {% include 'snippets/thread.html' with status=status|parent is_root=False %} + {% include 'feed/thread.html' with status=status|parent is_root=False %} {% endwith %} {% endif %} @@ -13,7 +13,7 @@ {% if depth <= max_depth and direction >= 0 %} {% for reply in status|replies %} {% with direction=1 %} - {% include 'snippets/thread.html' with status=reply is_root=False %} + {% include 'feed/thread.html' with status=reply is_root=False %} {% endwith %} {% endfor %} {% endif %} From 0c723eb550ea4c97f261ed9272e6b8852aa39bc5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 12:03:50 -0800 Subject: [PATCH 050/280] Fixes shelf page template path bug --- bookwyrm/templates/{ => user}/shelf.html | 28 +++++++++++------------- bookwyrm/tests/views/test_shelf.py | 2 +- bookwyrm/views/shelf.py | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) rename bookwyrm/templates/{ => user}/shelf.html (89%) diff --git a/bookwyrm/templates/shelf.html b/bookwyrm/templates/user/shelf.html similarity index 89% rename from bookwyrm/templates/shelf.html rename to bookwyrm/templates/user/shelf.html index da599c7d..c0083e17 100644 --- a/bookwyrm/templates/shelf.html +++ b/bookwyrm/templates/user/shelf.html @@ -1,21 +1,19 @@ -{% extends 'layout.html' %} +{% extends 'user/user_layout.html' %} {% load bookwyrm_tags %} -{% block content %} -
    -
    -

    - {% if is_self %}Your - {% else %} - {% include 'snippets/username.html' with user=user possessive=True %} - {% endif %} - shelves -

    -
    -
    - -{% include 'snippets/user_header.html' with user=user %} +{% block header %} +
    +

    + {% if is_self %}Your + {% else %} + {% include 'snippets/username.html' with user=user possessive=True %} + {% endif %} + shelves +

    +
    +{% endblock %} +{% block panel %}
    diff --git a/bookwyrm/tests/views/test_shelf.py b/bookwyrm/tests/views/test_shelf.py index 35e07953..95150fe9 100644 --- a/bookwyrm/tests/views/test_shelf.py +++ b/bookwyrm/tests/views/test_shelf.py @@ -41,7 +41,7 @@ class ShelfViews(TestCase): is_api.return_value = False result = view(request, self.local_user.username, shelf.identifier) self.assertIsInstance(result, TemplateResponse) - self.assertEqual(result.template_name, 'shelf.html') + self.assertEqual(result.template_name, 'user/shelf.html') self.assertEqual(result.status_code, 200) with patch('bookwyrm.views.shelf.is_api_request') as is_api: diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index 25275d67..05049109 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -62,7 +62,7 @@ class Shelf(View): 'books': [b.book for b in books], } - return TemplateResponse(request, 'shelf.html', data) + return TemplateResponse(request, 'user/shelf.html', data) @method_decorator(login_required, name='dispatch') # pylint: disable=unused-argument From 74d37c84be5c48fa0a146dcfc54c803091dcc722 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 15:46:20 -0800 Subject: [PATCH 051/280] Don't show the main button in the dropdown --- bookwyrm/templates/snippets/shelve_button_options.html | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/bookwyrm/templates/snippets/shelve_button_options.html b/bookwyrm/templates/snippets/shelve_button_options.html index af47ee23..2d7d9609 100644 --- a/bookwyrm/templates/snippets/shelve_button_options.html +++ b/bookwyrm/templates/snippets/shelve_button_options.html @@ -3,15 +3,15 @@ {% comparison_bool shelf.identifier active_shelf.shelf.identifier as is_current %} {% if dropdown %}
  • {% endif %} {% else %}
    - {% include 'snippets/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %} + {% include 'snippets/shelve_button/shelve_button_options.html' with class="shelf-option is-small" shelves=request.user.shelf_set.all active_shelf=active_shelf button_uuid=uuid %}
    - {% include 'snippets/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%} + {% include 'snippets/shelve_button/shelve_button_dropdown.html' with class="is-small" button_uuid=uuid%} {% endif %}
  • -{% include 'snippets/want_to_read_modal.html' with book=active_shelf.book controls_text="want-to-read" controls_uid=uuid no_body=True %} +{% include 'snippets/shelve_button/want_to_read_modal.html' with book=active_shelf.book controls_text="want-to-read" controls_uid=uuid no_body=True %} -{% include 'snippets/start_reading_modal.html' with book=active_shelf.book controls_text="start-reading" controls_uid=uuid %} +{% include 'snippets/shelve_button/start_reading_modal.html' with book=active_shelf.book controls_text="start-reading" controls_uid=uuid %} {% latest_read_through book request.user as readthrough %} -{% include 'snippets/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %} +{% include 'snippets/shelve_button/finish_reading_modal.html' with book=active_shelf.book controls_text="finish-reading" controls_uid=uuid readthrough=readthrough %} {% endwith %} {% endif %} diff --git a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html index d70765a3..47cf3505 100644 --- a/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html +++ b/bookwyrm/templates/snippets/shelve_button/shelve_button_dropdown.html @@ -6,5 +6,5 @@ {% endblock %} {% block dropdown-list %} -{% include 'snippets/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %} +{% include 'snippets/shelve_button/shelve_button_options.html' with active_shelf=active_shelf shelves=request.user.shelf_set.all dropdown=True class="shelf-option is-fullwidth is-small" %} {% endblock %} diff --git a/bookwyrm/templates/snippets/start_reading_modal.html b/bookwyrm/templates/snippets/shelve_button/start_reading_modal.html similarity index 100% rename from bookwyrm/templates/snippets/start_reading_modal.html rename to bookwyrm/templates/snippets/shelve_button/start_reading_modal.html diff --git a/bookwyrm/templates/snippets/want_to_read_modal.html b/bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html similarity index 100% rename from bookwyrm/templates/snippets/want_to_read_modal.html rename to bookwyrm/templates/snippets/shelve_button/want_to_read_modal.html From 661d49d9cc69915003ed14d3f4f4d4b797d5d4a6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 17:19:01 -0800 Subject: [PATCH 055/280] Ignore openlibrary editions with little to no metadata Also fixes the isbn problem --- bookwyrm/connectors/openlibrary.py | 31 +++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/bookwyrm/connectors/openlibrary.py b/bookwyrm/connectors/openlibrary.py index 55355131..cd196d27 100644 --- a/bookwyrm/connectors/openlibrary.py +++ b/bookwyrm/connectors/openlibrary.py @@ -27,9 +27,9 @@ class Connector(AbstractConnector): Mapping('series', formatter=get_first), Mapping('seriesNumber', remote_field='series_number'), Mapping('subjects'), - Mapping('subjectPlaces'), - Mapping('isbn13', formatter=get_first), - Mapping('isbn10', formatter=get_first), + Mapping('subjectPlaces', remote_field='subject_places'), + Mapping('isbn13', remote_field='isbn_13', formatter=get_first), + Mapping('isbn10', remote_field='isbn_10', formatter=get_first), Mapping('lccn', formatter=get_first), Mapping( 'oclcNumber', remote_field='oclc_numbers', @@ -144,9 +144,34 @@ class Connector(AbstractConnector): # we can mass download edition data from OL to avoid repeatedly querying edition_options = self.load_edition_data(work.openlibrary_key) for edition_data in edition_options.get('entries'): + # does this edition have ANY interesting data? + if ignore_edition(edition_data): + continue self.create_edition_from_data(work, edition_data) +def ignore_edition(edition_data): + ''' don't load a million editions that have no metadata ''' + # an isbn, we love to see it + if edition_data.get('isbn_13') or edition_data.get('isbn_10'): + print(edition_data.get('isbn_10')) + return False + # grudgingly, oclc can stay + if edition_data.get('oclc_numbers'): + print(edition_data.get('oclc_numbers')) + return False + # if it has a cover it can stay + if edition_data.get('covers'): + print(edition_data.get('covers')) + return False + # keep non-english editions + if edition_data.get('languages') and \ + 'languages/eng' not in str(edition_data.get('languages')): + print(edition_data.get('languages')) + return False + return True + + def get_description(description_blob): ''' descriptions can be a string or a dict ''' if isinstance(description_blob, dict): From 9833f5a03da6ae2b4a8d298c99506316ccac0fb5 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 17:36:24 -0800 Subject: [PATCH 056/280] Tests creating editions --- .../connectors/test_openlibrary_connector.py | 16 ++++++++++++++++ bookwyrm/tests/data/ol_edition.json | 1 + 2 files changed, 17 insertions(+) diff --git a/bookwyrm/tests/connectors/test_openlibrary_connector.py b/bookwyrm/tests/connectors/test_openlibrary_connector.py index dc4c5f5b..c277ba04 100644 --- a/bookwyrm/tests/connectors/test_openlibrary_connector.py +++ b/bookwyrm/tests/connectors/test_openlibrary_connector.py @@ -190,3 +190,19 @@ class Openlibrary(TestCase): ''' detect if the loaded json is an edition ''' edition = pick_default_edition(self.edition_list_data['entries']) self.assertEqual(edition['key'], '/books/OL9788823M') + + + def test_create_edition_from_data(self): + ''' okay but can it actually create an edition with proper metadata ''' + work = models.Work.objects.create(title='Hello') + result = self.connector.create_edition_from_data( + work, self.edition_data) + self.assertEqual(result.parent_work, work) + self.assertEqual(result.title, 'Sabriel') + self.assertEqual(result.isbn_10, '0060273224') + self.assertIsNotNone(result.description) + self.assertEqual(result.languages[0], 'English') + self.assertEqual(result.publishers[0], 'Harper Trophy') + self.assertEqual(result.pages, 491) + self.assertEqual(result.subjects[0], 'Fantasy.') + self.assertEqual(result.physical_format, 'Hardcover') diff --git a/bookwyrm/tests/data/ol_edition.json b/bookwyrm/tests/data/ol_edition.json index 459e9dff..2423364b 100644 --- a/bookwyrm/tests/data/ol_edition.json +++ b/bookwyrm/tests/data/ol_edition.json @@ -9,6 +9,7 @@ "518848" ] }, + "physical_format": "Hardcover", "lc_classifications": [ "PZ7.N647 Sab 1995" ], From 45ac13a7fffe00337127b5b82d6a569cc2703ed3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 18:30:07 -0800 Subject: [PATCH 057/280] Clear unused editions with poor metadata --- .../management/commands/remove_editions.py | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 bookwyrm/management/commands/remove_editions.py diff --git a/bookwyrm/management/commands/remove_editions.py b/bookwyrm/management/commands/remove_editions.py new file mode 100644 index 00000000..c5153f44 --- /dev/null +++ b/bookwyrm/management/commands/remove_editions.py @@ -0,0 +1,34 @@ +''' PROCEED WITH CAUTION: this permanently deletes book data ''' +from django.core.management.base import BaseCommand +from django.db.models import Count, Q +from bookwyrm import models + + +def remove_editions(): + ''' combine duplicate editions and update related models ''' + # not in use + filters = {'%s__isnull' % r.name: True \ + for r in models.Edition._meta.related_objects} + # no cover, no identifying fields + filters['cover'] = '' + null_fields = {'%s__isnull' % f: True for f in \ + ['isbn_10', 'isbn_13', 'oclc_number']} + + editions = models.Edition.objects.filter( + Q(languages=[]) | Q(languages__contains=['English']), + **filters, **null_fields + ).annotate(Count('parent_work__editions')).filter( + # mustn't be the only edition for the work + parent_work__editions__count__gt=1 + ) + print(editions.count()) + editions.delete() + + +class Command(BaseCommand): + ''' dedplucate allllll the book data models ''' + help = 'merges duplicate book data' + # pylint: disable=no-self-use,unused-argument + def handle(self, *args, **options): + ''' run deudplications ''' + remove_editions() From fe088f21fb495b27beee8b1ca178ddc7176f9830 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 21:00:36 -0800 Subject: [PATCH 058/280] Model and migration for lists --- .../migrations/0041_auto_20210131_0500.py | 65 +++++++++++++++ bookwyrm/models/__init__.py | 1 + bookwyrm/models/list.py | 83 +++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 bookwyrm/migrations/0041_auto_20210131_0500.py create mode 100644 bookwyrm/models/list.py diff --git a/bookwyrm/migrations/0041_auto_20210131_0500.py b/bookwyrm/migrations/0041_auto_20210131_0500.py new file mode 100644 index 00000000..eb00e3f3 --- /dev/null +++ b/bookwyrm/migrations/0041_auto_20210131_0500.py @@ -0,0 +1,65 @@ +# Generated by Django 3.0.7 on 2021-01-31 05:00 + +import bookwyrm.models.base_model +import bookwyrm.models.fields +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0040_auto_20210122_0057'), + ] + + operations = [ + migrations.CreateModel( + name='List', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), + ('name', bookwyrm.models.fields.CharField(max_length=100)), + ('description', bookwyrm.models.fields.TextField(blank=True, null=True)), + ('privacy', bookwyrm.models.fields.CharField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255)), + ('curation', bookwyrm.models.fields.CharField(choices=[('closed', 'Closed'), ('open', 'Open'), ('moderated', 'Moderated')], default='closed', max_length=255)), + ], + options={ + 'abstract': False, + }, + bases=(bookwyrm.models.base_model.OrderedCollectionMixin, models.Model), + ), + migrations.CreateModel( + name='ListItem', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_date', models.DateTimeField(auto_now_add=True)), + ('updated_date', models.DateTimeField(auto_now=True)), + ('remote_id', bookwyrm.models.fields.RemoteIdField(max_length=255, null=True, validators=[bookwyrm.models.fields.validate_remote_id])), + ('notes', bookwyrm.models.fields.TextField(blank=True, null=True)), + ('approved', models.BooleanField(default=True)), + ('order', bookwyrm.models.fields.IntegerField(blank=True, null=True)), + ('added_by', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)), + ('book', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='bookwyrm.Edition')), + ('book_list', bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='bookwyrm.List')), + ('endorsement', models.ManyToManyField(related_name='endorsers', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ('-created_date',), + 'unique_together': {('book', 'book_list')}, + }, + bases=(bookwyrm.models.base_model.ActivitypubMixin, models.Model), + ), + migrations.AddField( + model_name='list', + name='books', + field=models.ManyToManyField(through='bookwyrm.ListItem', to='bookwyrm.Edition'), + ), + migrations.AddField( + model_name='list', + name='user', + field=bookwyrm.models.fields.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL), + ), + ] diff --git a/bookwyrm/models/__init__.py b/bookwyrm/models/__init__.py index b232e98f..0aef6385 100644 --- a/bookwyrm/models/__init__.py +++ b/bookwyrm/models/__init__.py @@ -7,6 +7,7 @@ from .author import Author from .connector import Connector from .shelf import Shelf, ShelfBook +from .list import List, ListItem from .status import Status, GeneratedNote, Review, Comment, Quotation from .status import Boost diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py new file mode 100644 index 00000000..2bc49261 --- /dev/null +++ b/bookwyrm/models/list.py @@ -0,0 +1,83 @@ +''' make a list of books!! ''' +from django.db import models + +from bookwyrm import activitypub +from .base_model import ActivitypubMixin, BookWyrmModel +from .base_model import OrderedCollectionMixin +from . import fields + + +CurationType = models.TextChoices('Curation', [ + 'closed', + 'open', + 'moderated', +]) + +class List(OrderedCollectionMixin, BookWyrmModel): + ''' a list of books ''' + name = fields.CharField(max_length=100) + user = fields.ForeignKey( + 'User', on_delete=models.PROTECT, activitypub_field='owner') + description = fields.TextField(blank=True, null=True) + privacy = fields.CharField( + max_length=255, + default='public', + choices=fields.PrivacyLevels.choices + ) + curation = fields.CharField( + max_length=255, + default='closed', + choices=CurationType.choices + ) + books = models.ManyToManyField( + 'Edition', + symmetrical=False, + through='ListItem', + through_fields=('book_list', 'book'), + ) + @property + def collection_queryset(self): + ''' list of books for this shelf, overrides OrderedCollectionMixin ''' + return self.books.all().order_by('listitem') + + +class ListItem(ActivitypubMixin, BookWyrmModel): + ''' ok ''' + book = fields.ForeignKey( + 'Edition', on_delete=models.PROTECT, activitypub_field='object') + book_list = fields.ForeignKey( + 'List', on_delete=models.CASCADE, activitypub_field='target') + added_by = fields.ForeignKey( + 'User', + on_delete=models.PROTECT, + activitypub_field='actor' + ) + notes = fields.TextField(blank=True, null=True) + approved = models.BooleanField(default=True) + order = fields.IntegerField(blank=True, null=True) + endorsement = models.ManyToManyField('User', related_name='endorsers') + + activity_serializer = activitypub.AddBook + + def to_add_activity(self, user): + ''' AP for shelving a book''' + return activitypub.Add( + id='%s#add' % self.remote_id, + actor=user.remote_id, + object=self.book.to_activity(), + target=self.book_list.remote_id, + ).serialize() + + def to_remove_activity(self, user): + ''' AP for un-shelving a book''' + return activitypub.Remove( + id='%s#remove' % self.remote_id, + actor=user.remote_id, + object=self.book.to_activity(), + target=self.book_list.to_activity() + ).serialize() + + class Meta: + ''' an opinionated constraint! you can't put a book on a list twice ''' + unique_together = ('book', 'book_list') + ordering = ('-created_date',) From af65509527dfa4d46b63267bfb49e6d642f4efbd Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 21:03:50 -0800 Subject: [PATCH 059/280] stub test for list model --- bookwyrm/tests/models/test_list.py | 32 ++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 bookwyrm/tests/models/test_list.py diff --git a/bookwyrm/tests/models/test_list.py b/bookwyrm/tests/models/test_list.py new file mode 100644 index 00000000..bca63e16 --- /dev/null +++ b/bookwyrm/tests/models/test_list.py @@ -0,0 +1,32 @@ +''' testing models ''' +from django.test import TestCase + +from bookwyrm import models, settings + + +class List(TestCase): + ''' some activitypub oddness ahead ''' + def setUp(self): + ''' look, a list ''' + self.user = models.User.objects.create_user( + 'mouse', 'mouse@mouse.mouse', 'mouseword', + local=True, localname='mouse') + self.list = models.List.objects.create( + name='Test List', user=self.user) + + def test_remote_id(self): + ''' shelves use custom remote ids ''' + expected_id = 'https://%s/user/mouse/list/%d' % \ + (settings.DOMAIN, self.list.id) + self.assertEqual(self.list.get_remote_id(), expected_id) + + + def test_to_activity(self): + ''' jsonify it ''' + activity_json = self.list.to_activity() + self.assertIsInstance(activity_json, dict) + self.assertEqual(activity_json['id'], self.list.remote_id) + self.assertEqual(activity_json['totalItems'], 0) + self.assertEqual(activity_json['type'], 'OrderedCollection') + self.assertEqual(activity_json['name'], 'Test List') + self.assertEqual(activity_json['owner'], self.user.remote_id) From 0815b36ec9af2b44f0c4eeab19dcfa188f4603c3 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sat, 30 Jan 2021 21:33:41 -0800 Subject: [PATCH 060/280] Adds basic view and template files --- bookwyrm/templates/layout.html | 5 ++- bookwyrm/templates/lists/list.html | 7 ++++ bookwyrm/templates/lists/lists.html | 16 +++++++++ bookwyrm/urls.py | 9 +++-- bookwyrm/views/__init__.py | 1 + bookwyrm/views/list.py | 53 +++++++++++++++++++++++++++++ 6 files changed, 88 insertions(+), 3 deletions(-) create mode 100644 bookwyrm/templates/lists/list.html create mode 100644 bookwyrm/templates/lists/lists.html create mode 100644 bookwyrm/views/list.py diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 81782970..1a6c065e 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -53,13 +53,16 @@ From 1e9189d43c952d4529d274268f84a33d92922664 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 31 Jan 2021 10:56:40 -0800 Subject: [PATCH 068/280] Suggest recently edited books if we're out of user books --- bookwyrm/templates/lists/list.html | 6 +++--- bookwyrm/templates/snippets/privacy-icons.html | 16 ++++++++-------- bookwyrm/views/list.py | 10 +++++++++- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/bookwyrm/templates/lists/list.html b/bookwyrm/templates/lists/list.html index 8f171bb4..38be3edf 100644 --- a/bookwyrm/templates/lists/list.html +++ b/bookwyrm/templates/lists/list.html @@ -5,7 +5,7 @@

    {{ list.name }} {% include 'snippets/privacy-icons.html' with item=list %}

    -

    Created by {% include 'snippets/username.html' with user=list.user %}

    +

    Created {% if list.curation != 'open' %} and curated{% endif %} by {% include 'snippets/username.html' with user=list.user %}

    {% include 'snippets/trimmed_text.html' with full=list.description %}
    {% if request.user == list.user %} @@ -64,7 +64,7 @@ {% if not list.curation == 'closed' or request.user == list.user %}
    -

    Add Books

    +

    {% if list.curation == 'open' or request.user == list.user %}Add{% else %}Suggest{% endif %} Books

    {% for book in suggested_books %}
    @@ -75,7 +75,7 @@
    {% csrf_token %} - +
    diff --git a/bookwyrm/templates/snippets/privacy-icons.html b/bookwyrm/templates/snippets/privacy-icons.html index 793fbc8b..c917f553 100644 --- a/bookwyrm/templates/snippets/privacy-icons.html +++ b/bookwyrm/templates/snippets/privacy-icons.html @@ -1,18 +1,18 @@ {% if item.privacy == 'public' %} - - Public post + + Public {% elif item.privacy == 'unlisted' %} - - Unlisted post + + Unlisted {% elif item.privacy == 'followers' %} - - Followers-only post + + Followers-only {% else %} - - Private post + + Private {% endif %} diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 63f99c4a..c126102d 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -55,11 +55,19 @@ class List(View): suggestions = request.user.shelfbook_set.filter( ~Q(book__in=book_list.books.all()) ) + suggestions = [s.book for s in suggestions[:5]] + if len(suggestions) < 5: + suggestions += [s.default_edition for s in \ + models.Work.objects.filter( + ~Q(editions__in=book_list.books.all()), + ).order_by('-updated_date') + ][:5 - len(suggestions)] + data = { 'title': '%s | Lists' % book_list.name, 'list': book_list, - 'suggested_books': [s.book for s in suggestions[:5]], + 'suggested_books': suggestions, 'list_form': forms.ListForm(instance=book_list), } return TemplateResponse(request, 'lists/list.html', data) From 6a68fe9475cf2eb9fdaecc4683e7096a2d9efbd6 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Sun, 31 Jan 2021 11:11:26 -0800 Subject: [PATCH 069/280] Search for books to add to lists --- bookwyrm/connectors/connector_manager.py | 4 ++-- bookwyrm/connectors/self_connector.py | 11 +++++++--- bookwyrm/templates/layout.html | 2 +- bookwyrm/templates/lists/list.html | 17 ++++++++++++++ bookwyrm/views/list.py | 28 +++++++++++++++--------- 5 files changed, 46 insertions(+), 16 deletions(-) diff --git a/bookwyrm/connectors/connector_manager.py b/bookwyrm/connectors/connector_manager.py index d3b01f7a..a63a788e 100644 --- a/bookwyrm/connectors/connector_manager.py +++ b/bookwyrm/connectors/connector_manager.py @@ -35,10 +35,10 @@ def search(query, min_confidence=0.1): return results -def local_search(query, min_confidence=0.1): +def local_search(query, min_confidence=0.1, raw=False): ''' only look at local search results ''' connector = load_connector(models.Connector.objects.get(local=True)) - return connector.search(query, min_confidence=min_confidence) + return connector.search(query, min_confidence=min_confidence, raw=raw) def first_search_result(query, min_confidence=0.1): diff --git a/bookwyrm/connectors/self_connector.py b/bookwyrm/connectors/self_connector.py index c5d58a59..f57fbc1c 100644 --- a/bookwyrm/connectors/self_connector.py +++ b/bookwyrm/connectors/self_connector.py @@ -11,7 +11,8 @@ from .abstract_connector import AbstractConnector, SearchResult class Connector(AbstractConnector): ''' instantiate a connector ''' - def search(self, query, min_confidence=0.1): + # pylint: disable=arguments-differ + def search(self, query, min_confidence=0.1, raw=False): ''' search your local database ''' if not query: return [] @@ -22,10 +23,14 @@ class Connector(AbstractConnector): results = search_title_author(query, min_confidence) search_results = [] for result in results: - search_results.append(self.format_search_result(result)) + if raw: + search_results.append(result) + else: + search_results.append(self.format_search_result(result)) if len(search_results) >= 10: break - search_results.sort(key=lambda r: r.confidence, reverse=True) + if not raw: + search_results.sort(key=lambda r: r.confidence, reverse=True) return search_results diff --git a/bookwyrm/templates/layout.html b/bookwyrm/templates/layout.html index 1a6c065e..fe8a7509 100644 --- a/bookwyrm/templates/layout.html +++ b/bookwyrm/templates/layout.html @@ -59,10 +59,10 @@ Feed - {% endif %} Lists + {% endif %}

    - {% include 'snippets/username.html' with user=result show_full=True %} - {% include 'snippets/follow_button.html' with user=result %} - - {% endfor %} +
    +

    Matching Users

    + {% if not user_results %} +

    No users found for "{{ query }}"

    + {% endif %} +
      + {% for result in user_results %} +
    • + {% include 'snippets/avatar.html' with user=result %} + {% include 'snippets/username.html' with user=result show_full=True %} + {% include 'snippets/follow_button.html' with user=result %} +
    • + {% endfor %} +
    +
    +
    +

    Lists

    + {% if not list_results %} +

    No lists found for "{{ query }}"

    + {% endif %} + {% for result in list_results %} +
    + +
    + {% endfor %} +
    {% endwith %} diff --git a/bookwyrm/views/search.py b/bookwyrm/views/search.py index 8066777a..a4cd7337 100644 --- a/bookwyrm/views/search.py +++ b/bookwyrm/views/search.py @@ -10,7 +10,7 @@ from django.views import View from bookwyrm import models from bookwyrm.connectors import connector_manager from bookwyrm.utils import regex -from .helpers import is_api_request +from .helpers import is_api_request, privacy_filter from .helpers import handle_remote_webfinger @@ -32,7 +32,7 @@ class Search(View): if re.match(r'\B%s' % regex.full_username, query): handle_remote_webfinger(query) - # do a local user search + # do a user search user_results = models.User.objects.annotate( similarity=Greatest( TrigramSimilarity('username', query), @@ -42,12 +42,25 @@ class Search(View): similarity__gt=0.5, ).order_by('-similarity')[:10] + # any relevent lists? + list_results = privacy_filter( + request.user, models.List.objects, ['public', 'followers'] + ).annotate( + similarity=Greatest( + TrigramSimilarity('name', query), + TrigramSimilarity('description', query), + ) + ).filter( + similarity__gt=0.1, + ).order_by('-similarity')[:10] + book_results = connector_manager.search( query, min_confidence=min_confidence) data = { 'title': 'Search Results', 'book_results': book_results, 'user_results': user_results, + 'list_results': list_results, 'query': query, } return TemplateResponse(request, 'search_results.html', data) From 3e3b21c46aced1ba1a8fe3c0c696115eb85fae24 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 1 Feb 2021 12:03:11 -0800 Subject: [PATCH 088/280] Fixes ap serialization of list lists --- bookwyrm/views/list.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index 50cab1fd..b7a8dad0 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -84,7 +84,7 @@ class List(View): return HttpResponseNotFound() if is_api_request(request): - return ActivitypubResponse(book_list.to_activity()) + return ActivitypubResponse(book_list.to_activity(**request.GET)) query = request.GET.get('q') suggestions = None From c7914d1394c1459b2a65dadc3d45476f7f49230a Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Mon, 1 Feb 2021 13:06:06 -0800 Subject: [PATCH 089/280] Fixes search tests --- bookwyrm/tests/views/test_search.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bookwyrm/tests/views/test_search.py b/bookwyrm/tests/views/test_search.py index e5cba99c..655b4563 100644 --- a/bookwyrm/tests/views/test_search.py +++ b/bookwyrm/tests/views/test_search.py @@ -83,6 +83,7 @@ class ShelfViews(TestCase): ) request = self.factory.get('', {'q': 'Test Book'}) + request.user = self.local_user with patch('bookwyrm.views.search.is_api_request') as is_api: is_api.return_value = False with patch( @@ -99,6 +100,7 @@ class ShelfViews(TestCase): ''' searches remote connectors ''' view = views.Search.as_view() request = self.factory.get('', {'q': 'mouse'}) + request.user = self.local_user with patch('bookwyrm.views.search.is_api_request') as is_api: is_api.return_value = False with patch('bookwyrm.connectors.connector_manager.search'): From e53b4e57fa65f8394a90a09485fd7a24c4255f0c Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 2 Feb 2021 09:37:46 -0800 Subject: [PATCH 090/280] Federating lists and shelves --- bookwyrm/activitypub/ordered_collection.py | 11 +++--- bookwyrm/activitypub/verbs.py | 2 +- bookwyrm/models/base_model.py | 18 +++++----- bookwyrm/models/list.py | 9 ++--- bookwyrm/models/shelf.py | 6 +--- bookwyrm/views/goal.py | 7 +++- bookwyrm/views/list.py | 40 +++++++++++++++++++++- bookwyrm/views/shelf.py | 10 ++++-- 8 files changed, 74 insertions(+), 29 deletions(-) diff --git a/bookwyrm/activitypub/ordered_collection.py b/bookwyrm/activitypub/ordered_collection.py index 9aeaf664..335723f4 100644 --- a/bookwyrm/activitypub/ordered_collection.py +++ b/bookwyrm/activitypub/ordered_collection.py @@ -1,5 +1,5 @@ ''' defines activitypub collections (lists) ''' -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import List from .base_activity import ActivityObject @@ -10,9 +10,12 @@ class OrderedCollection(ActivityObject): ''' structure of an ordered collection activity ''' totalItems: int first: str - last: str = '' - name: str = '' - owner: str = '' + last: str = None + name: str = None + summary: str = None + owner: str = None + to: List[str] = field(default_factory=lambda: []) + cc: List[str] = field(default_factory=lambda: []) type: str = 'OrderedCollection' diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 6977ee8e..5502ced0 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -18,7 +18,7 @@ class Create(Verb): ''' Create activity ''' to: List cc: List - signature: Signature + signature: Signature = None type: str = 'Create' diff --git a/bookwyrm/models/base_model.py b/bookwyrm/models/base_model.py index 3f1ed66d..889eb5f4 100644 --- a/bookwyrm/models/base_model.py +++ b/bookwyrm/models/base_model.py @@ -161,16 +161,18 @@ class ActivitypubMixin: ''' returns the object wrapped in a Create activity ''' activity_object = self.to_activity(**kwargs) - signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key)) - content = activity_object['content'] - signed_message = signer.sign(SHA256.new(content.encode('utf8'))) + signature = None create_id = self.remote_id + '/activity' + if 'content' in activity_object: + signer = pkcs1_15.new(RSA.import_key(user.key_pair.private_key)) + content = activity_object['content'] + signed_message = signer.sign(SHA256.new(content.encode('utf8'))) - signature = activitypub.Signature( - creator='%s#main-key' % user.remote_id, - created=activity_object['published'], - signatureValue=b64encode(signed_message).decode('utf8') - ) + signature = activitypub.Signature( + creator='%s#main-key' % user.remote_id, + created=activity_object['published'], + signatureValue=b64encode(signed_message).decode('utf8') + ) return activitypub.Create( id=create_id, diff --git a/bookwyrm/models/list.py b/bookwyrm/models/list.py index 6ade16de..801a3f2a 100644 --- a/bookwyrm/models/list.py +++ b/bookwyrm/models/list.py @@ -19,12 +19,9 @@ class List(OrderedCollectionMixin, BookWyrmModel): name = fields.CharField(max_length=100) user = fields.ForeignKey( 'User', on_delete=models.PROTECT, activitypub_field='owner') - description = fields.TextField(blank=True, null=True) - privacy = fields.CharField( - max_length=255, - default='public', - choices=fields.PrivacyLevels.choices - ) + description = fields.TextField( + blank=True, null=True, activitypub_field='summary') + privacy = fields.PrivacyField() curation = fields.CharField( max_length=255, default='closed', diff --git a/bookwyrm/models/shelf.py b/bookwyrm/models/shelf.py index 84575137..87200f38 100644 --- a/bookwyrm/models/shelf.py +++ b/bookwyrm/models/shelf.py @@ -15,11 +15,7 @@ class Shelf(OrderedCollectionMixin, BookWyrmModel): user = fields.ForeignKey( 'User', on_delete=models.PROTECT, activitypub_field='owner') editable = models.BooleanField(default=True) - privacy = fields.CharField( - max_length=255, - default='public', - choices=fields.PrivacyLevels.choices - ) + privacy = fields.PrivacyField() books = models.ManyToManyField( 'Edition', symmetrical=False, diff --git a/bookwyrm/views/goal.py b/bookwyrm/views/goal.py index ac7a1702..0496848f 100644 --- a/bookwyrm/views/goal.py +++ b/bookwyrm/views/goal.py @@ -71,10 +71,15 @@ class Goal(View): broadcast( request.user, status.to_create_activity(request.user), + privacy=status.privacy, software='bookwyrm') # re-format the activity for non-bookwyrm servers remote_activity = status.to_create_activity(request.user, pure=True) - broadcast(request.user, remote_activity, software='other') + broadcast( + request.user, + remote_activity, + privacy=status.privacy, + software='other') return redirect(request.headers.get('Referer', '/')) diff --git a/bookwyrm/views/list.py b/bookwyrm/views/list.py index b7a8dad0..ce0cedb4 100644 --- a/bookwyrm/views/list.py +++ b/bookwyrm/views/list.py @@ -11,6 +11,7 @@ from django.views.decorators.http import require_POST from bookwyrm import forms, models from bookwyrm.activitypub import ActivitypubResponse +from bookwyrm.broadcast import broadcast from bookwyrm.connectors import connector_manager from .helpers import is_api_request, object_visible_to_user, privacy_filter from .helpers import get_user_from_username @@ -48,6 +49,14 @@ class Lists(View): if not form.is_valid(): return redirect('lists') book_list = form.save() + + # let the world know + broadcast( + request.user, + book_list.to_create_activity(request.user), + privacy=book_list.privacy, + software='bookwyrm' + ) return redirect(book_list.local_path) class UserLists(View): @@ -128,6 +137,13 @@ class List(View): if not form.is_valid(): return redirect('list', book_list.id) book_list = form.save() + # let the world know + broadcast( + request.user, + book_list.to_update_activity(request.user), + privacy=book_list.privacy, + software='bookwyrm' + ) return redirect(book_list.local_path) @@ -161,6 +177,13 @@ class Curate(View): if approved: suggestion.approved = True suggestion.save() + # let the world know + broadcast( + request.user, + suggestion.to_add_activity(request.user), + privacy=book_list.privacy, + software='bookwyrm' + ) else: suggestion.delete() return redirect('list-curate', book_list.id) @@ -177,11 +200,18 @@ def add_book(request, list_id): # do you have permission to add to the list? if request.user == book_list.user or book_list.curation == 'open': # go ahead and add it - models.ListItem.objects.create( + item = models.ListItem.objects.create( book=book, book_list=book_list, added_by=request.user, ) + # let the world know + broadcast( + request.user, + item.to_add_activity(request.user), + privacy=book_list.privacy, + software='bookwyrm' + ) elif book_list.curation == 'curated': # make a pending entry models.ListItem.objects.create( @@ -206,5 +236,13 @@ def remove_book(request, list_id): if not book_list.user == request.user and not item.added_by == request.user: return HttpResponseNotFound() + activity = item.to_remove_activity(request.user) item.delete() + # let the world know + broadcast( + request.user, + activity, + privacy=book_list.privacy, + software='bookwyrm' + ) return redirect('list', list_id) diff --git a/bookwyrm/views/shelf.py b/bookwyrm/views/shelf.py index e4c49261..ebeb6b52 100644 --- a/bookwyrm/views/shelf.py +++ b/bookwyrm/views/shelf.py @@ -138,7 +138,12 @@ def shelve(request): pass shelfbook = models.ShelfBook.objects.create( book=book, shelf=desired_shelf, added_by=request.user) - broadcast(request.user, shelfbook.to_add_activity(request.user)) + broadcast( + request.user, + shelfbook.to_add_activity(request.user), + privacy=shelfbook.shelf.privacy, + software='bookwyrm' + ) # post about "want to read" shelves if desired_shelf.identifier == 'to-read': @@ -146,7 +151,6 @@ def shelve(request): request.user, desired_shelf, book, - privacy=desired_shelf.privacy ) return redirect('/') @@ -169,4 +173,4 @@ def handle_unshelve(user, book, shelf): activity = row.to_remove_activity(user) row.delete() - broadcast(user, activity) + broadcast(user, activity, privacy=shelf.privacy, software='bookwyrm') From 23ce639e5c54404c73852810613ebb5b8ac9a303 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 2 Feb 2021 09:41:24 -0800 Subject: [PATCH 091/280] migration to change privacy field type --- .../migrations/0042_auto_20210201_2108.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 bookwyrm/migrations/0042_auto_20210201_2108.py diff --git a/bookwyrm/migrations/0042_auto_20210201_2108.py b/bookwyrm/migrations/0042_auto_20210201_2108.py new file mode 100644 index 00000000..95a144de --- /dev/null +++ b/bookwyrm/migrations/0042_auto_20210201_2108.py @@ -0,0 +1,28 @@ +# Generated by Django 3.0.7 on 2021-02-01 21:08 + +import bookwyrm.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('bookwyrm', '0041_auto_20210131_1614'), + ] + + operations = [ + migrations.AlterModelOptions( + name='list', + options={'ordering': ('-updated_date',)}, + ), + migrations.AlterField( + model_name='list', + name='privacy', + field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + ), + migrations.AlterField( + model_name='shelf', + name='privacy', + field=bookwyrm.models.fields.PrivacyField(choices=[('public', 'Public'), ('unlisted', 'Unlisted'), ('followers', 'Followers'), ('direct', 'Direct')], default='public', max_length=255), + ), + ] From d2fc3febb0b063e0bcac6f3585bcbaa1a09c014f Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 2 Feb 2021 09:44:29 -0800 Subject: [PATCH 092/280] Show reading activity panel when no readthroughs exist --- bookwyrm/templates/book.html | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bookwyrm/templates/book.html b/bookwyrm/templates/book.html index 4a95237c..51811d17 100644 --- a/bookwyrm/templates/book.html +++ b/bookwyrm/templates/book.html @@ -147,7 +147,7 @@ {% endfor %} - {% if readthroughs.exists %} + {% if request.user.is_authenticated %}

    Your reading activity

    @@ -155,6 +155,9 @@ {% include 'snippets/toggle/open_button.html' with text="Add read dates" icon="plus" class="is-small" controls_text="add-readthrough" %}
    + {% if not readthroughs.exists %} +

    You don't have any reading activity for this book.

    + {% endif %}
    diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index bd0715a0..ed7b1f5f 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -1,5 +1,6 @@ ''' invites when registration is closed ''' from django.contrib.auth.decorators import login_required, permission_required +from django.core.paginator import Paginator from django.http import HttpResponseBadRequest from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse @@ -7,6 +8,7 @@ from django.utils.decorators import method_decorator from django.views import View from bookwyrm import forms, models +from bookwyrm.settings import PAGE_LENGTH # pylint: disable= no-self-use @@ -18,10 +20,18 @@ class ManageInvites(View): ''' create invites ''' def get(self, request): ''' invite management page ''' + try: + page = int(request.GET.get('page', 1)) + except ValueError: + page = 1 + + paginated = Paginator(models.SiteInvite.objects.filter( + user=request.user + ).order_by('-created_date'), PAGE_LENGTH) + data = { 'title': 'Invitations', - 'invites': models.SiteInvite.objects.filter( - user=request.user).order_by('-created_date'), + 'invites': paginated.page(page), 'form': forms.CreateInviteForm(), } return TemplateResponse(request, 'settings/manage_invites.html', data) From baed291889eb3672c6c503b7b023e28a72ca26ce Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 14:45:39 -0800 Subject: [PATCH 264/280] Don't broadcast after saving remote server --- bookwyrm/models/user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index a1f927b2..094a13d5 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -319,7 +319,7 @@ def set_remote_server(user_id): actor_parts = urlparse(user.remote_id) user.federated_server = \ get_or_create_remote_server(actor_parts.netloc) - user.save() + user.save(broadcast=False) if user.bookwyrm_user: get_remote_reviews.delay(user.outbox) From 384187a263c649c4b563b1d422dc09e5c0f07ede Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 15:21:37 -0800 Subject: [PATCH 265/280] Moves create invite form to top of invite page --- .../templates/settings/manage_invites.html | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/bookwyrm/templates/settings/manage_invites.html b/bookwyrm/templates/settings/manage_invites.html index 42668a3d..086615a9 100644 --- a/bookwyrm/templates/settings/manage_invites.html +++ b/bookwyrm/templates/settings/manage_invites.html @@ -2,29 +2,6 @@ {% block header %}Invites{% endblock %} {% load humanize %} {% block panel %} -
    - - - - - - - - {% if not invites %} - - {% endif %} - {% for invite in invites %} - - - - - - - {% endfor %} -
    LinkExpiresMax usesTimes used
    No active invites
    {{ invite.link }}{{ invite.expiry|naturaltime }}{{ invite.use_limit }}{{ invite.times_used }}
    - {% include 'snippets/pagination.html' with page=invites path=request.path %} -
    -

    Generate New Invite

    @@ -48,4 +25,27 @@
    + +
    + + + + + + + + {% if not invites %} + + {% endif %} + {% for invite in invites %} + + + + + + + {% endfor %} +
    LinkExpiresMax usesTimes used
    No active invites
    {{ invite.link }}{{ invite.expiry|naturaltime }}{{ invite.use_limit }}{{ invite.times_used }}
    + {% include 'snippets/pagination.html' with page=invites path=request.path %} +
    {% endblock %} From 23fb5f62a2e58d4869b1ab79aa72f105dda305f7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 15:25:26 -0800 Subject: [PATCH 266/280] Keep invite settings in form after save --- bookwyrm/views/invite.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/bookwyrm/views/invite.py b/bookwyrm/views/invite.py index ed7b1f5f..6b3611fc 100644 --- a/bookwyrm/views/invite.py +++ b/bookwyrm/views/invite.py @@ -46,7 +46,15 @@ class ManageInvites(View): invite.user = request.user invite.save() - return redirect('/settings/invites') + paginated = Paginator(models.SiteInvite.objects.filter( + user=request.user + ).order_by('-created_date'), PAGE_LENGTH) + data = { + 'title': 'Invitations', + 'invites': paginated.page(1), + 'form': form + } + return TemplateResponse(request, 'settings/manage_invites.html', data) class Invite(View): From 6e6bcb2f4833e27d4c626d9d6d7c73c2616db5ff Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 15:51:02 -0800 Subject: [PATCH 267/280] gotta simplify the add activity --- bookwyrm/activitypub/__init__.py | 2 +- bookwyrm/activitypub/verbs.py | 11 ++--- bookwyrm/models/list.py | 2 +- bookwyrm/tests/views/test_inbox.py | 75 ++++++++++++++++++++++++++++++ 4 files changed, 80 insertions(+), 10 deletions(-) diff --git a/bookwyrm/activitypub/__init__.py b/bookwyrm/activitypub/__init__.py index f06154ab..fdfbb1f0 100644 --- a/bookwyrm/activitypub/__init__.py +++ b/bookwyrm/activitypub/__init__.py @@ -15,7 +15,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, AddListItem, Remove +from .verbs import Add, Remove from .verbs import Announce, Like # this creates a list of all the Activity types that we can serialize, diff --git a/bookwyrm/activitypub/verbs.py b/bookwyrm/activitypub/verbs.py index 6f1a4d44..1236338b 100644 --- a/bookwyrm/activitypub/verbs.py +++ b/bookwyrm/activitypub/verbs.py @@ -121,6 +121,9 @@ class Add(Verb): target: str object: Edition type: str = 'Add' + notes: str = None + order: int = 0 + approved: bool = True def action(self): ''' add obj to collection ''' @@ -131,14 +134,6 @@ class Add(Verb): self.to_model(model=model) -@dataclass(init=False) -class AddListItem(Add): - '''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/models/list.py b/bookwyrm/models/list.py index ef48ed95..1b14c2aa 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.AddListItem + activity_serializer = activitypub.Add object_field = 'book' collection_field = 'book_list' diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index c2658917..45966d59 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -612,6 +612,81 @@ class Inbox(TestCase): self.assertEqual(shelf.books.first(), book) +# def test_handle_tag_book(self): +# ''' tagging a book ''' +# work = models.Work.objects.create(title='work title') +# book = models.Edition.objects.create( +# title='Test', remote_id='https://bookwyrm.social/book/37292', +# parent_work=work) +# +# activity = { +# "id": "https://bookwyrm.social/shelfbook/6189#add", +# "type": "Add", +# "actor": "https://example.com/users/rat", +# "object": { +# "type": "Edition", +# "title": "Test Title", +# "work": work.remote_id, +# "id": "https://bookwyrm.social/book/37292", +# }, +# "target": "", +# "@context": "https://www.w3.org/ns/activitystreams" +# } +# views.inbox.activity_task(activity) +# self.assertEqual(shelf.books.first(), book) + + + @responses.activate + def test_handle_add_book_to_list(self): + ''' listing a book ''' + work = models.Work.objects.create(title='work title') + book = models.Edition.objects.create( + title='Test', remote_id='https://bookwyrm.social/book/37292', + parent_work=work) + + responses.add( + responses.GET, + 'https://bookwyrm.social/user/mouse/list/to-read', + json={ + "id": "https://example.com/list/22", + "type": "BookList", + "totalItems": 1, + "first": "https://example.com/list/22?page=1", + "last": "https://example.com/list/22?page=1", + "name": "Test List", + "owner": "https://example.com/user/mouse", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "https://example.com/user/mouse/followers" + ], + "summary": "summary text", + "curation": "curated", + "@context": "https://www.w3.org/ns/activitystreams" + } + ) + + activity = { + "id": "https://bookwyrm.social/listbook/6189#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": { + "type": "Edition", + "title": "Test Title", + "work": work.remote_id, + "id": "https://bookwyrm.social/book/37292", + }, + "target": "https://bookwyrm.social/user/mouse/list/to-read", + "@context": "https://www.w3.org/ns/activitystreams" + } + views.inbox.activity_task(activity) + + booklist = models.List.objects.get() + self.assertEqual(booklist.name, 'Test List') + self.assertEqual(booklist.books.first(), book) + + def test_handle_update_user(self): ''' update an existing user ''' # we only do this with remote users From 4d0e52bf517a53d46c070a0d82b4efa89ced70a7 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 17:18:25 -0800 Subject: [PATCH 268/280] Test tag and list add --- bookwyrm/activitypub/base_activity.py | 3 +- bookwyrm/models/activitypub_mixin.py | 2 +- bookwyrm/models/tag.py | 19 ++++---- bookwyrm/tests/views/test_inbox.py | 67 +++++++++++++++++---------- bookwyrm/tests/views/test_tag.py | 15 ++++++ bookwyrm/views/tag.py | 7 ++- 6 files changed, 73 insertions(+), 40 deletions(-) diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index c360711d..57f1a713 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -82,7 +82,8 @@ class ActivityObject: if activity_objects: value = naive_parse(activity_objects, value) else: - value = naive_parse(activity_objects, value, serializer=field.type) + value = naive_parse( + activity_objects, value, serializer=field.type) except KeyError: if field.default == MISSING and \ diff --git a/bookwyrm/models/activitypub_mixin.py b/bookwyrm/models/activitypub_mixin.py index 7ea632b3..fe89f267 100644 --- a/bookwyrm/models/activitypub_mixin.py +++ b/bookwyrm/models/activitypub_mixin.py @@ -330,7 +330,7 @@ class OrderedCollectionMixin(OrderedCollectionPageMixin): def to_activity(self, **kwargs): ''' an ordered collection of the specified model queryset ''' return self.to_ordered_collection( - self.collection_queryset, **kwargs).serialize() + self.collection_queryset, **kwargs) class CollectionItemMixin(ActivitypubMixin): diff --git a/bookwyrm/models/tag.py b/bookwyrm/models/tag.py index 12645b9e..83359170 100644 --- a/bookwyrm/models/tag.py +++ b/bookwyrm/models/tag.py @@ -1,6 +1,7 @@ ''' models for storing different kinds of Activities ''' import urllib.parse +from django.apps import apps from django.db import models from bookwyrm import activitypub @@ -15,17 +16,15 @@ class Tag(OrderedCollectionMixin, BookWyrmModel): name = fields.CharField(max_length=100, unique=True) identifier = models.CharField(max_length=100) - @classmethod - def book_queryset(cls, identifier): - ''' county of books associated with this tag ''' - return cls.objects.filter( - identifier=identifier - ).order_by('-updated_date') - @property - def collection_queryset(self): - ''' books associated with this tag ''' - return self.book_queryset(self.identifier) + def books(self): + ''' count of books associated with this tag ''' + edition_model = apps.get_model('bookwyrm.Edition', require_ready=True) + return edition_model.objects.filter( + usertag__tag__identifier=self.identifier + ).order_by('-created_date').distinct() + + collection_queryset = books def get_remote_id(self): ''' tag should use identifier not id in remote_id ''' diff --git a/bookwyrm/tests/views/test_inbox.py b/bookwyrm/tests/views/test_inbox.py index 45966d59..ff55ad04 100644 --- a/bookwyrm/tests/views/test_inbox.py +++ b/bookwyrm/tests/views/test_inbox.py @@ -612,30 +612,6 @@ class Inbox(TestCase): self.assertEqual(shelf.books.first(), book) -# def test_handle_tag_book(self): -# ''' tagging a book ''' -# work = models.Work.objects.create(title='work title') -# book = models.Edition.objects.create( -# title='Test', remote_id='https://bookwyrm.social/book/37292', -# parent_work=work) -# -# activity = { -# "id": "https://bookwyrm.social/shelfbook/6189#add", -# "type": "Add", -# "actor": "https://example.com/users/rat", -# "object": { -# "type": "Edition", -# "title": "Test Title", -# "work": work.remote_id, -# "id": "https://bookwyrm.social/book/37292", -# }, -# "target": "", -# "@context": "https://www.w3.org/ns/activitystreams" -# } -# views.inbox.activity_task(activity) -# self.assertEqual(shelf.books.first(), book) - - @responses.activate def test_handle_add_book_to_list(self): ''' listing a book ''' @@ -687,6 +663,49 @@ class Inbox(TestCase): self.assertEqual(booklist.books.first(), book) + @responses.activate + def test_handle_tag_book(self): + ''' listing a book ''' + work = models.Work.objects.create(title='work title') + book = models.Edition.objects.create( + title='Test', remote_id='https://bookwyrm.social/book/37292', + parent_work=work) + + responses.add( + responses.GET, + 'https://www.example.com/tag/cool-tag', + json={ + "id": "https://1b1a78582461.ngrok.io/tag/tag", + "type": "OrderedCollection", + "totalItems": 0, + "first": "https://1b1a78582461.ngrok.io/tag/tag?page=1", + "last": "https://1b1a78582461.ngrok.io/tag/tag?page=1", + "name": "cool tag", + "@context": "https://www.w3.org/ns/activitystreams" + } + ) + + activity = { + "id": "https://bookwyrm.social/listbook/6189#add", + "type": "Add", + "actor": "https://example.com/users/rat", + "object": { + "type": "Edition", + "title": "Test Title", + "work": work.remote_id, + "id": "https://bookwyrm.social/book/37292", + }, + "target": "https://www.example.com/tag/cool-tag", + "@context": "https://www.w3.org/ns/activitystreams" + } + views.inbox.activity_task(activity) + + tag = models.Tag.objects.get() + self.assertFalse(models.List.objects.exists()) + self.assertEqual(tag.name, 'cool tag') + self.assertEqual(tag.books.first(), book) + + def test_handle_update_user(self): ''' update an existing user ''' # we only do this with remote users diff --git a/bookwyrm/tests/views/test_tag.py b/bookwyrm/tests/views/test_tag.py index 21a7e22e..ef809b46 100644 --- a/bookwyrm/tests/views/test_tag.py +++ b/bookwyrm/tests/views/test_tag.py @@ -59,6 +59,21 @@ class TagViews(TestCase): self.assertEqual(result.status_code, 200) + def test_tag_page_activitypub_page(self): + ''' there are so many views, this just makes sure it LOADS ''' + view = views.Tag.as_view() + with patch('bookwyrm.models.activitypub_mixin.broadcast_task.delay'): + tag = models.Tag.objects.create(name='hi there') + models.UserTag.objects.create( + tag=tag, user=self.local_user, book=self.book) + request = self.factory.get('', {'page': 1}) + with patch('bookwyrm.views.tag.is_api_request') as is_api: + is_api.return_value = True + result = view(request, tag.identifier) + self.assertIsInstance(result, ActivitypubResponse) + self.assertEqual(result.status_code, 200) + + def test_tag(self): ''' add a tag to a book ''' view = views.AddTag.as_view() diff --git a/bookwyrm/views/tag.py b/bookwyrm/views/tag.py index b50bc0ef..710f9415 100644 --- a/bookwyrm/views/tag.py +++ b/bookwyrm/views/tag.py @@ -16,12 +16,11 @@ class Tag(View): ''' tag page ''' def get(self, request, tag_id): ''' see books related to a tag ''' - tag_obj = models.Tag.objects.filter(identifier=tag_id).first() - if not tag_obj: - return HttpResponseNotFound() + tag_obj = get_object_or_404(models.Tag, identifier=tag_id) if is_api_request(request): - return ActivitypubResponse(tag_obj.to_activity(**request.GET)) + return ActivitypubResponse( + tag_obj.to_activity(**request.GET), safe=False) books = models.Edition.objects.filter( usertag__tag__identifier=tag_id From fba53c72e0d1ff2c2efc12967696386a7880b96d Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 17:19:47 -0800 Subject: [PATCH 269/280] default safe mode for activity serialization --- bookwyrm/activitypub/response.py | 2 +- bookwyrm/views/tag.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/bookwyrm/activitypub/response.py b/bookwyrm/activitypub/response.py index bbc44c4d..8f3c050b 100644 --- a/bookwyrm/activitypub/response.py +++ b/bookwyrm/activitypub/response.py @@ -9,7 +9,7 @@ class ActivitypubResponse(JsonResponse): configures some stuff beforehand. Made to be a drop-in replacement of JsonResponse. """ - def __init__(self, data, encoder=ActivityEncoder, safe=True, + def __init__(self, data, encoder=ActivityEncoder, safe=False, json_dumps_params=None, **kwargs): if 'content_type' not in kwargs: diff --git a/bookwyrm/views/tag.py b/bookwyrm/views/tag.py index 710f9415..502f5ea5 100644 --- a/bookwyrm/views/tag.py +++ b/bookwyrm/views/tag.py @@ -1,6 +1,5 @@ ''' tagging views''' from django.contrib.auth.decorators import login_required -from django.http import HttpResponseNotFound from django.shortcuts import get_object_or_404, redirect from django.template.response import TemplateResponse from django.utils.decorators import method_decorator @@ -20,7 +19,7 @@ class Tag(View): if is_api_request(request): return ActivitypubResponse( - tag_obj.to_activity(**request.GET), safe=False) + tag_obj.to_activity(**request.GET)) books = models.Edition.objects.filter( usertag__tag__identifier=tag_id From 744de313c8ab2febf33a060a343726365b583467 Mon Sep 17 00:00:00 2001 From: Mouse Reeve Date: Tue, 23 Feb 2021 17:23:11 -0800 Subject: [PATCH 270/280] Makes comment and fav/boost buttons the same color when selected --- bookwyrm/templates/snippets/boost_button.html | 2 +- bookwyrm/templates/snippets/fav_button.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bookwyrm/templates/snippets/boost_button.html b/bookwyrm/templates/snippets/boost_button.html index 08daae64..bf914379 100644 --- a/bookwyrm/templates/snippets/boost_button.html +++ b/bookwyrm/templates/snippets/boost_button.html @@ -11,7 +11,7 @@
    {% csrf_token %} -