Merge branch 'main' into production

This commit is contained in:
Mouse Reeve 2021-09-08 12:18:35 -07:00
commit 2022b3a035
14 changed files with 126 additions and 25 deletions

View file

@ -469,7 +469,7 @@ def handle_boost_task(boost_id):
old_versions = models.Boost.objects.filter( old_versions = models.Boost.objects.filter(
boosted_status__id=boosted.id, boosted_status__id=boosted.id,
created_date__lt=instance.created_date, created_date__lt=instance.created_date,
).values_list("id", flat=True) )
for stream in streams.values(): for stream in streams.values():
audience = stream.get_stores_for_object(instance) audience = stream.get_stores_for_object(instance)

View file

@ -134,6 +134,7 @@ class EditUserForm(CustomForm):
"email", "email",
"summary", "summary",
"show_goal", "show_goal",
"show_suggested_users",
"manually_approves_followers", "manually_approves_followers",
"default_post_privacy", "default_post_privacy",
"discoverable", "discoverable",

View file

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

View file

@ -4,6 +4,7 @@ import re
from django.contrib.postgres.search import SearchVectorField from django.contrib.postgres.search import SearchVectorField
from django.contrib.postgres.indexes import GinIndex from django.contrib.postgres.indexes import GinIndex
from django.db import models from django.db import models
from django.db import transaction
from django.dispatch import receiver from django.dispatch import receiver
from model_utils import FieldTracker from model_utils import FieldTracker
from model_utils.managers import InheritanceManager from model_utils.managers import InheritanceManager
@ -361,4 +362,6 @@ def preview_image(instance, *args, **kwargs):
changed_fields = instance.field_tracker.changed() changed_fields = instance.field_tracker.changed()
if len(changed_fields) > 0: 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)
)

View file

@ -28,7 +28,7 @@ class FederatedServer(BookWyrmModel):
def block(self): def block(self):
"""block a server""" """block a server"""
self.status = "blocked" self.status = "blocked"
self.save() self.save(update_fields=["status"])
# deactivate all associated users # deactivate all associated users
self.user_set.filter(is_active=True).update( self.user_set.filter(is_active=True).update(
@ -45,7 +45,7 @@ class FederatedServer(BookWyrmModel):
def unblock(self): def unblock(self):
"""unblock a server""" """unblock a server"""
self.status = "federated" self.status = "federated"
self.save() self.save(update_fields=["status"])
self.user_set.filter(deactivation_reason="domain_block").update( self.user_set.filter(deactivation_reason="domain_block").update(
is_active=True, deactivation_reason=None is_active=True, deactivation_reason=None

View file

@ -122,8 +122,12 @@ class User(OrderedCollectionPageMixin, AbstractUser):
updated_date = models.DateTimeField(auto_now=True) updated_date = models.DateTimeField(auto_now=True)
last_active_date = models.DateTimeField(default=timezone.now) last_active_date = models.DateTimeField(default=timezone.now)
manually_approves_followers = fields.BooleanField(default=False) manually_approves_followers = fields.BooleanField(default=False)
# options to turn features on and off
show_goal = models.BooleanField(default=True) show_goal = models.BooleanField(default=True)
show_suggested_users = models.BooleanField(default=True)
discoverable = fields.BooleanField(default=False) discoverable = fields.BooleanField(default=False)
preferred_timezone = models.CharField( preferred_timezone = models.CharField(
choices=[(str(tz), str(tz)) for tz in pytz.all_timezones], choices=[(str(tz), str(tz)) for tz in pytz.all_timezones],
default=str(pytz.utc), default=str(pytz.utc),

View file

@ -86,10 +86,12 @@ class SuggestedUsers(RedisStore):
values = self.get_store(self.store_id(user), withscores=True) values = self.get_store(self.store_id(user), withscores=True)
results = [] results = []
# annotate users with mutuals and shared book counts # 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) counts = self.get_counts_from_rank(rank)
try: 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: except models.User.DoesNotExist as err:
# if this happens, the suggestions are janked way up # if this happens, the suggestions are janked way up
logger.exception(err) logger.exception(err)
@ -97,6 +99,8 @@ class SuggestedUsers(RedisStore):
user.mutuals = counts["mutuals"] user.mutuals = counts["mutuals"]
# user.shared_books = counts["shared_books"] # user.shared_books = counts["shared_books"]
results.append(user) results.append(user)
if len(results) >= 5:
break
return results return results
@ -178,13 +182,21 @@ def update_suggestions_on_unfollow(sender, instance, **kwargs):
@receiver(signals.post_save, sender=models.User) @receiver(signals.post_save, sender=models.User)
# pylint: disable=unused-argument, too-many-arguments # pylint: disable=unused-argument, too-many-arguments
def add_new_user(sender, instance, created, update_fields=None, **kwargs): def update_user(sender, instance, created, update_fields=None, **kwargs):
"""a new user, wow how cool""" """an updated user, neat"""
# a new user is found, create suggestions for them # a new user is found, create suggestions for them
if created and instance.local: if created and instance.local:
rerank_suggestions_task.delay(instance.id) 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 return
# this happens on every save, not just when discoverability changes, annoyingly # 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) 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") @app.task(queue="low_priority")
def rerank_suggestions_task(user_id): def rerank_suggestions_task(user_id):
"""do the hard work in celery""" """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""" """remove a specific user from a specific user's suggestions"""
suggested_user = models.User.objects.get(id=suggested_user_id) suggested_user = models.User.objects.get(id=suggested_user_id)
suggested_users.remove_suggestion(user_id, suggested_user) 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)

View file

@ -22,7 +22,7 @@
{% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %} {% blocktrans with tab_key=tab.key %}load <span data-poll="stream/{{ tab_key }}">0</span> unread status(es){% endblocktrans %}
</a> </a>
{% 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 %} {% now 'Y' as year %}
<section class="block"> <section class="block">
{% include 'snippets/goal_card.html' with year=year %} {% include 'snippets/goal_card.html' with year=year %}
@ -37,7 +37,7 @@
<div class="block content"> <div class="block content">
<p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p> <p>{% trans "There aren't any activities right now! Try following a user to get started" %}</p>
{% if suggested_users %} {% if request.user.show_suggested_users and suggested_users %}
{# suggested users for when things are very lonely #} {# suggested users for when things are very lonely #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
{% endif %} {% endif %}
@ -46,7 +46,7 @@
{% for activity in activities %} {% 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 #} {# suggested users on the first page, two statuses down #}
{% include 'feed/suggested_users.html' with suggested_users=suggested_users %} {% include 'feed/suggested_users.html' with suggested_users=suggested_users %}
{% endif %} {% endif %}

View file

@ -1,6 +1,15 @@
{% load i18n %} {% load i18n %}
<section class="block"> <section class="block">
<h2 class="title is-5">{% trans "Who to follow" %}</h2> <header class="columns">
<div class="column">
<h2 class="title is-5">{% trans "Who to follow" %}</h2>
</div>
<form class="column is-narrow" action="{% url 'hide-suggestions' %}" method="POST">
{% csrf_token %}
{% trans "Don't show suggested users" as button_text %}
<button type="submit" class="delete" title="{{ button_text }}">{{ button_text }}</button>
</form>
</header>
{% include 'snippets/suggested_users.html' with suggested_users=suggested_users %} {% include 'snippets/suggested_users.html' with suggested_users=suggested_users %}
<a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a> <a class="help" href="{% url 'directory' %}">{% trans "View directory" %} <span class="icon icon-arrow-right"></span></a>
</section> </section>

View file

@ -43,9 +43,19 @@
</div> </div>
<div class="block"> <div class="block">
<label class="checkbox label" for="id_show_goal"> <label class="checkbox label" for="id_show_goal">
{% trans "Show set reading goal prompt in feed:" %} {% trans "Show reading goal prompt in feed:" %}
{{ form.show_goal }} {{ form.show_goal }}
</label> </label>
<label class="checkbox label" for="id_show_goal">
{% trans "Show suggested users:" %}
{{ form.show_suggested_users }}
</label>
<label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
</div> </div>
<div class="block"> <div class="block">
<label class="checkbox label" for="id_manually_approves_followers"> <label class="checkbox label" for="id_manually_approves_followers">
@ -61,14 +71,6 @@
{{ form.default_post_privacy }} {{ form.default_post_privacy }}
</div> </div>
</div> </div>
<div class="block">
<label class="checkbox label" for="id_discoverable">
{% trans "Show this account in suggested users:" %}
{{ form.discoverable }}
</label>
{% url 'directory' as path %}
<p class="help">{% blocktrans %}Your account will show up in the <a href="{{ path }}">directory</a>, and may be recommended to other BookWyrm users.{% endblocktrans %}</p>
</div>
<div class="block"> <div class="block">
<label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label> <label class="label" for="id_preferred_timezone">{% trans "Preferred Timezone: " %}</label>
<div class="select"> <div class="select">

View file

@ -80,7 +80,9 @@ class FederationViews(TestCase):
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True 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() server.refresh_from_db()
self.remote_user.refresh_from_db() self.remote_user.refresh_from_db()
@ -118,7 +120,11 @@ class FederationViews(TestCase):
request.user = self.local_user request.user = self.local_user
request.user.is_superuser = True 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() server.refresh_from_db()
self.remote_user.refresh_from_db() self.remote_user.refresh_from_db()
self.assertEqual(server.status, "federated") self.assertEqual(server.status, "federated")

View file

@ -215,6 +215,7 @@ urlpatterns = [
views.Following.as_view(), views.Following.as_view(),
name="user-following", name="user-following",
), ),
re_path(r"^hide-suggestions/?$", views.hide_suggestions, name="hide-suggestions"),
# lists # lists
re_path(r"%s/lists/?$" % USER_PATH, views.UserLists.as_view(), name="user-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"), re_path(r"^list/?$", views.Lists.as_view(), name="lists"),

View file

@ -43,6 +43,6 @@ from .shelf import shelve, unshelve
from .site import Site from .site import Site
from .status import CreateStatus, DeleteStatus, DeleteAndRedraft from .status import CreateStatus, DeleteStatus, DeleteAndRedraft
from .updates import get_notification_count, get_unread_status_count 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 .user_admin import UserAdmin, UserAdminList
from .wellknown import * from .wellknown import *

View file

@ -1,8 +1,11 @@
""" non-interactive pages """ """ non-interactive pages """
from django.contrib.auth.decorators import login_required
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.shortcuts import redirect
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.utils import timezone from django.utils import timezone
from django.views import View from django.views import View
from django.views.decorators.http import require_POST
from bookwyrm import models from bookwyrm import models
from bookwyrm.activitypub import ActivitypubResponse from bookwyrm.activitypub import ActivitypubResponse
@ -118,3 +121,12 @@ class Following(View):
"follow_list": paginated.get_page(request.GET.get("page")), "follow_list": paginated.get_page(request.GET.get("page")),
} }
return TemplateResponse(request, "user/relationships/following.html", data) 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", "/"))