diff --git a/bookwyrm/activitystreams.py b/bookwyrm/activitystreams.py index c32dfa35..18870853 100644 --- a/bookwyrm/activitystreams.py +++ b/bookwyrm/activitystreams.py @@ -469,7 +469,7 @@ def handle_boost_task(boost_id): old_versions = models.Boost.objects.filter( boosted_status__id=boosted.id, created_date__lt=instance.created_date, - ).values_list("id", flat=True) + ) for stream in streams.values(): audience = stream.get_stores_for_object(instance) diff --git a/bookwyrm/forms.py b/bookwyrm/forms.py index 69052605..96a4e303 100644 --- a/bookwyrm/forms.py +++ b/bookwyrm/forms.py @@ -134,6 +134,7 @@ class EditUserForm(CustomForm): "email", "summary", "show_goal", + "show_suggested_users", "manually_approves_followers", "default_post_privacy", "discoverable", diff --git a/bookwyrm/migrations/0089_user_show_suggested_users.py b/bookwyrm/migrations/0089_user_show_suggested_users.py new file mode 100644 index 00000000..047bb974 --- /dev/null +++ b/bookwyrm/migrations/0089_user_show_suggested_users.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.4 on 2021-09-08 16:28 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0088_auto_20210905_2233"), + ] + + operations = [ + migrations.AddField( + model_name="user", + name="show_suggested_users", + field=models.BooleanField(default=True), + ), + ] diff --git a/bookwyrm/models/book.py b/bookwyrm/models/book.py index 8bed6924..ec1c14ec 100644 --- a/bookwyrm/models/book.py +++ b/bookwyrm/models/book.py @@ -4,6 +4,7 @@ import re from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.indexes import GinIndex from django.db import models +from django.db import transaction from django.dispatch import receiver from model_utils import FieldTracker from model_utils.managers import InheritanceManager @@ -361,4 +362,6 @@ def preview_image(instance, *args, **kwargs): changed_fields = instance.field_tracker.changed() if len(changed_fields) > 0: - generate_edition_preview_image_task.delay(instance.id) + transaction.on_commit( + lambda: generate_edition_preview_image_task.delay(instance.id) + ) diff --git a/bookwyrm/models/federated_server.py b/bookwyrm/models/federated_server.py index e297c46c..2be13b38 100644 --- a/bookwyrm/models/federated_server.py +++ b/bookwyrm/models/federated_server.py @@ -28,7 +28,7 @@ class FederatedServer(BookWyrmModel): def block(self): """block a server""" self.status = "blocked" - self.save() + self.save(update_fields=["status"]) # deactivate all associated users self.user_set.filter(is_active=True).update( @@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel): def unblock(self): """unblock a server""" self.status = "federated" - self.save() + self.save(update_fields=["status"]) self.user_set.filter(deactivation_reason="domain_block").update( is_active=True, deactivation_reason=None diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 4b03f665..7500669f 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -122,8 +122,12 @@ class User(OrderedCollectionPageMixin, AbstractUser): updated_date = models.DateTimeField(auto_now=True) last_active_date = models.DateTimeField(default=timezone.now) manually_approves_followers = fields.BooleanField(default=False) + + # options to turn features on and off show_goal = models.BooleanField(default=True) + show_suggested_users = models.BooleanField(default=True) discoverable = fields.BooleanField(default=False) + preferred_timezone = models.CharField( choices=[(str(tz), str(tz)) for tz in pytz.all_timezones], default=str(pytz.utc), diff --git a/bookwyrm/suggested_users.py b/bookwyrm/suggested_users.py index 92902938..88343061 100644 --- a/bookwyrm/suggested_users.py +++ b/bookwyrm/suggested_users.py @@ -86,10 +86,12 @@ class SuggestedUsers(RedisStore): values = self.get_store(self.store_id(user), withscores=True) results = [] # annotate users with mutuals and shared book counts - for user_id, rank in values[:5]: + for user_id, rank in values: counts = self.get_counts_from_rank(rank) try: - user = models.User.objects.get(id=user_id) + user = models.User.objects.get( + id=user_id, is_active=True, bookwyrm_user=True + ) except models.User.DoesNotExist as err: # if this happens, the suggestions are janked way up logger.exception(err) @@ -97,6 +99,8 @@ class SuggestedUsers(RedisStore): user.mutuals = counts["mutuals"] # user.shared_books = counts["shared_books"] results.append(user) + if len(results) >= 5: + break return results @@ -178,13 +182,21 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs): @receiver(signals.post_save, sender=models.User) # pylint: disable=unused-argument, too-many-arguments -def add_new_user(sender, instance, created, update_fields=None, **kwargs): - """a new user, wow how cool""" +def update_user(sender, instance, created, update_fields=None, **kwargs): + """an updated user, neat""" # a new user is found, create suggestions for them if created and instance.local: rerank_suggestions_task.delay(instance.id) - if update_fields and not "discoverable" in update_fields: + # we know what fields were updated and discoverability didn't change + if not instance.bookwyrm_user or ( + update_fields and not "discoverable" in update_fields + ): + return + + # deleted the user + if not created and not instance.is_active: + remove_user_task.delay(instance.id) return # this happens on every save, not just when discoverability changes, annoyingly @@ -194,6 +206,25 @@ def add_new_user(sender, instance, created, update_fields=None, **kwargs): remove_user_task.delay(instance.id) +@receiver(signals.post_save, sender=models.FederatedServer) +def domain_level_update(sender, instance, created, update_fields=None, **kwargs): + """remove users on a domain block""" + if ( + not update_fields + or "status" not in update_fields + or instance.application_type != "bookwyrm" + ): + return + + if instance.status == "blocked": + bulk_remove_instance_task.delay(instance.id) + return + bulk_add_instance_task.delay(instance.id) + + +# ------------------- TASKS + + @app.task(queue="low_priority") def rerank_suggestions_task(user_id): """do the hard work in celery""" @@ -219,3 +250,17 @@ def remove_suggestion_task(user_id, suggested_user_id): """remove a specific user from a specific user's suggestions""" suggested_user = models.User.objects.get(id=suggested_user_id) suggested_users.remove_suggestion(user_id, suggested_user) + + +@app.task(queue="low_priority") +def bulk_remove_instance_task(instance_id): + """remove a bunch of users from recs""" + for user in models.User.objects.filter(federated_server__id=instance_id): + suggested_users.remove_object_from_related_stores(user) + + +@app.task(queue="low_priority") +def bulk_add_instance_task(instance_id): + """remove a bunch of users from recs""" + for user in models.User.objects.filter(federated_server__id=instance_id): + suggested_users.rerank_obj(user, update_only=False) diff --git a/bookwyrm/templates/feed/feed.html b/bookwyrm/templates/feed/feed.html index 265a467a..b8e351c9 100644 --- a/bookwyrm/templates/feed/feed.html +++ b/bookwyrm/templates/feed/feed.html @@ -22,7 +22,7 @@ {% blocktrans with tab_key=tab.key %}load 0 unread status(es){% endblocktrans %} -{% if request.user.show_goal and not goal and tab.key == streams.first.key %} +{% if request.user.show_goal and not goal and tab.key == 'home' %} {% now 'Y' as year %}
{% include 'snippets/goal_card.html' with year=year %} @@ -37,7 +37,7 @@

{% trans "There aren't any activities right now! Try following a user to get started" %}

- {% if suggested_users %} + {% if request.user.show_suggested_users and suggested_users %} {# suggested users for when things are very lonely #} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% endif %} @@ -46,7 +46,7 @@ {% for activity in activities %} -{% if not activities.number > 1 and forloop.counter0 == 2 and suggested_users %} +{% if request.user.show_suggested_users and not activities.number > 1 and forloop.counter0 == 2 and suggested_users %} {# suggested users on the first page, two statuses down #} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% endif %} diff --git a/bookwyrm/templates/feed/suggested_users.html b/bookwyrm/templates/feed/suggested_users.html index 1de1ae13..4e9f822b 100644 --- a/bookwyrm/templates/feed/suggested_users.html +++ b/bookwyrm/templates/feed/suggested_users.html @@ -1,6 +1,15 @@ {% load i18n %}
-

{% trans "Who to follow" %}

+
+
+

{% trans "Who to follow" %}

+
+
+ {% csrf_token %} + {% trans "Don't show suggested users" as button_text %} + +
+
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} {% trans "View directory" %}
diff --git a/bookwyrm/templates/preferences/edit_user.html b/bookwyrm/templates/preferences/edit_user.html index 6732b8ef..4dc792f9 100644 --- a/bookwyrm/templates/preferences/edit_user.html +++ b/bookwyrm/templates/preferences/edit_user.html @@ -43,9 +43,19 @@
+ + + {% url 'directory' as path %} +

{% blocktrans %}Your account will show up in the directory, and may be recommended to other BookWyrm users.{% endblocktrans %}

-
- - {% url 'directory' as path %} -

{% blocktrans %}Your account will show up in the directory, and may be recommended to other BookWyrm users.{% endblocktrans %}

-
diff --git a/bookwyrm/tests/views/test_federation.py b/bookwyrm/tests/views/test_federation.py index ce5de7e3..493aaf93 100644 --- a/bookwyrm/tests/views/test_federation.py +++ b/bookwyrm/tests/views/test_federation.py @@ -80,7 +80,9 @@ class FederationViews(TestCase): request.user = self.local_user request.user.is_superuser = True - view(request, server.id) + with patch("bookwyrm.suggested_users.bulk_remove_instance_task.delay") as mock: + view(request, server.id) + self.assertEqual(mock.call_count, 1) server.refresh_from_db() self.remote_user.refresh_from_db() @@ -118,7 +120,11 @@ class FederationViews(TestCase): request.user = self.local_user request.user.is_superuser = True - views.federation.unblock_server(request, server.id) + with patch("bookwyrm.suggested_users.bulk_add_instance_task.delay") as mock: + views.federation.unblock_server(request, server.id) + self.assertEqual(mock.call_count, 1) + self.assertEqual(mock.call_args[0][0], server.id) + server.refresh_from_db() self.remote_user.refresh_from_db() self.assertEqual(server.status, "federated") diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 991114fa..9ae1b822 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -215,6 +215,7 @@ urlpatterns = [ views.Following.as_view(), name="user-following", ), + re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"), # lists re_path(r"%s/lists/?$" % USER_PATH, views.UserLists.as_view(), name="user-lists"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index 841026a5..5142d532 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -43,6 +43,6 @@ from .shelf import shelve, unshelve from .site import Site from .status import CreateStatus, DeleteStatus, DeleteAndRedraft from .updates import get_notification_count, get_unread_status_count -from .user import User, Followers, Following +from .user import User, Followers, Following, hide_suggestions from .user_admin import UserAdmin, UserAdminList from .wellknown import * diff --git a/bookwyrm/views/user.py b/bookwyrm/views/user.py index 87e42b4e..e335b02c 100644 --- a/bookwyrm/views/user.py +++ b/bookwyrm/views/user.py @@ -1,8 +1,11 @@ """ non-interactive pages """ +from django.contrib.auth.decorators import login_required from django.core.paginator import Paginator +from django.shortcuts import redirect from django.template.response import TemplateResponse from django.utils import timezone from django.views import View +from django.views.decorators.http import require_POST from bookwyrm import models from bookwyrm.activitypub import ActivitypubResponse @@ -118,3 +121,12 @@ class Following(View): "follow_list": paginated.get_page(request.GET.get("page")), } return TemplateResponse(request, "user/relationships/following.html", data) + + +@require_POST +@login_required +def hide_suggestions(request): + """not everyone wants user suggestions""" + request.user.show_suggested_users = False + request.user.save(broadcast=False, update_fields=["show_suggested_users"]) + return redirect(request.headers.get("Referer", "/"))