diff --git a/README.md b/README.md index 558d42d45..f8b2eb1f6 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ Keep track of what books you've read, and what books you'd like to read in the f Federation allows you to interact with users on other instances and services, and also shares metadata about books and authors, which collaboratively builds a decentralized database of books. ### Privacy and moderation -Users and administrators can control who can see thier posts and what other instances to federate with. +Users and administrators can control who can see their posts and what other instances to federate with. ## Tech Stack Web backend diff --git a/bookwyrm/activitypub/base_activity.py b/bookwyrm/activitypub/base_activity.py index e942c9aeb..fa845f124 100644 --- a/bookwyrm/activitypub/base_activity.py +++ b/bookwyrm/activitypub/base_activity.py @@ -271,7 +271,7 @@ def resolve_remote_id( try: data = get_data(remote_id) except ConnectorException: - logger.exception("Could not connect to host for remote_id: %s", remote_id) + logger.info("Could not connect to host for remote_id: %s", remote_id) return None # determine the model implicitly, if not provided diff --git a/bookwyrm/connectors/abstract_connector.py b/bookwyrm/connectors/abstract_connector.py index c1ee7fe78..8ae93926a 100644 --- a/bookwyrm/connectors/abstract_connector.py +++ b/bookwyrm/connectors/abstract_connector.py @@ -222,7 +222,7 @@ def dict_from_mappings(data, mappings): return result -def get_data(url, params=None, timeout=10): +def get_data(url, params=None, timeout=settings.QUERY_TIMEOUT): """wrapper for request.get""" # check if the url is blocked raise_not_valid_url(url) diff --git a/bookwyrm/migrations/0166_sitesettings_imports_enabled.py b/bookwyrm/migrations/0166_sitesettings_imports_enabled.py new file mode 100644 index 000000000..ccf4ef374 --- /dev/null +++ b/bookwyrm/migrations/0166_sitesettings_imports_enabled.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.16 on 2022-11-17 21:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0165_alter_inviterequest_answer"), + ] + + operations = [ + migrations.AddField( + model_name="sitesettings", + name="imports_enabled", + field=models.BooleanField(default=True), + ), + ] diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index 3c1494204..9e97ede9a 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -86,6 +86,9 @@ class SiteSettings(SiteModel): admin_email = models.EmailField(max_length=255, null=True, blank=True) footer_item = models.TextField(null=True, blank=True) + # controls + imports_enabled = models.BooleanField(default=True) + field_tracker = FieldTracker(fields=["name", "instance_tagline", "logo"]) @classmethod diff --git a/bookwyrm/models/user.py b/bookwyrm/models/user.py index 5f7b00d87..e48d86572 100644 --- a/bookwyrm/models/user.py +++ b/bookwyrm/models/user.py @@ -244,9 +244,10 @@ class User(OrderedCollectionPageMixin, AbstractUser): def admins(cls): """Get a queryset of the admins for this instance""" return cls.objects.filter( - models.Q(user_permissions__name__in=["moderate_user", "moderate_post"]) - | models.Q(is_superuser=True) - ) + models.Q(groups__name__in=["moderator", "admin"]) + | models.Q(is_superuser=True), + is_active=True, + ).distinct() def update_active_date(self): """this user is here! they are doing things!""" diff --git a/bookwyrm/settings.py b/bookwyrm/settings.py index 1f9cd87ed..0fcc00590 100644 --- a/bookwyrm/settings.py +++ b/bookwyrm/settings.py @@ -11,7 +11,7 @@ from django.utils.translation import gettext_lazy as _ env = Env() env.read_env() DOMAIN = env("DOMAIN") -VERSION = "0.4.6" +VERSION = "0.5.1" RELEASE_API = env( "RELEASE_API", diff --git a/bookwyrm/static/css/themes/bookwyrm-dark.scss b/bookwyrm/static/css/themes/bookwyrm-dark.scss index a2eb94efb..b98422688 100644 --- a/bookwyrm/static/css/themes/bookwyrm-dark.scss +++ b/bookwyrm/static/css/themes/bookwyrm-dark.scss @@ -92,6 +92,10 @@ $family-secondary: $family-sans-serif; color: $grey-light !important; } +#qrcode svg { + background-color: #a6a6a6; +} + @import "../bookwyrm.scss"; @import "../vendor/icons.css"; @import "../vendor/shepherd.scss"; diff --git a/bookwyrm/templates/about/about.html b/bookwyrm/templates/about/about.html index 481ecda99..c446e0cf2 100644 --- a/bookwyrm/templates/about/about.html +++ b/bookwyrm/templates/about/about.html @@ -11,7 +11,7 @@ {% block about_content %} {# seven day cache #} -{% cache 604800 about_page %} +{% cache 604800 about_page_superlatives %} {% get_book_superlatives as superlatives %}
@@ -97,6 +97,7 @@

+{% endcache %}
@@ -145,5 +146,4 @@
-{% endcache %} {% endblock %} diff --git a/bookwyrm/templates/embed-layout.html b/bookwyrm/templates/embed-layout.html index 233ba387f..6a8d77016 100644 --- a/bookwyrm/templates/embed-layout.html +++ b/bookwyrm/templates/embed-layout.html @@ -1,14 +1,13 @@ {% load layout %} {% load i18n %} +{% load sass_tags %} {% load static %} {% block title %}BookWyrm{% endblock %} - {{ site.name }} - - - + diff --git a/bookwyrm/templates/import/import.html b/bookwyrm/templates/import/import.html index a2924703c..141e5671e 100644 --- a/bookwyrm/templates/import/import.html +++ b/bookwyrm/templates/import/import.html @@ -8,83 +8,94 @@

{% trans "Import Books" %}

- {% if recent_avg_hours or recent_avg_minutes %} -
-

- {% if recent_avg_hours %} - {% blocktrans trimmed with hours=recent_avg_hours|floatformat:0|intcomma %} - On average, recent imports have taken {{ hours }} hours. - {% endblocktrans %} - {% else %} - {% blocktrans trimmed with minutes=recent_avg_minutes|floatformat:0|intcomma %} - On average, recent imports have taken {{ minutes }} minutes. - {% endblocktrans %} + {% if site.imports_enabled %} + {% if recent_avg_hours or recent_avg_minutes %} +

+

+ {% if recent_avg_hours %} + {% blocktrans trimmed with hours=recent_avg_hours|floatformat:0|intcomma %} + On average, recent imports have taken {{ hours }} hours. + {% endblocktrans %} + {% else %} + {% blocktrans trimmed with minutes=recent_avg_minutes|floatformat:0|intcomma %} + On average, recent imports have taken {{ minutes }} minutes. + {% endblocktrans %} + {% endif %} +

+
{% endif %} + +
+ {% csrf_token %} + +
+
+
+ + +
+ +
+ +

+ {% blocktrans trimmed %} + You can download your Goodreads data from the + Import/Export page + of your Goodreads account. + {% endblocktrans %} +

+
+ +
+ + {{ import_form.csv_file }} +
+
+ +
+
+ +
+
+ + {% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %} +
+
+
+ +
+ {% else %} +
+

+ +

+

+ {% trans "Imports are temporarily disabled; thank you for your patience." %}

{% endif %} - -
- {% csrf_token %} - -
-
-
- - -
- -
- -

- {% blocktrans trimmed %} - You can download your Goodreads data from the - Import/Export page - of your Goodreads account. - {% endblocktrans %} -

-
- -
- - {{ import_form.csv_file }} -
-
- -
-
- -
-
- - {% include 'snippets/privacy_select.html' with no_label=True privacy_uuid="import" %} -
-
-
- -
diff --git a/bookwyrm/templates/lists/embed-list.html b/bookwyrm/templates/lists/embed-list.html index 186681670..d9a50a464 100644 --- a/bookwyrm/templates/lists/embed-list.html +++ b/bookwyrm/templates/lists/embed-list.html @@ -5,7 +5,9 @@ {% load group_tags %} {% load markdown %} -{% block title %}{% blocktrans with list_name=list.name owner=list.user.display_name %}{{ list_name }}, a list by {{owner}}{% endblocktrans %}{% endblock title %} +{% block title %}{% blocktrans trimmed with list_name=list.name owner=list.user.display_name %} +{{ list_name }}, a list by {{owner}} +{% endblocktrans %}{% endblock title %} {% block content %}
diff --git a/bookwyrm/templates/preferences/2fa.html b/bookwyrm/templates/preferences/2fa.html index 10de2993f..27ef026fb 100644 --- a/bookwyrm/templates/preferences/2fa.html +++ b/bookwyrm/templates/preferences/2fa.html @@ -45,7 +45,7 @@

{% trans "Scan the QR code with your authentication app and then enter the code from your app below to confirm your app is set up." %}

-
{{ qrcode | safe }}
+
{{ qrcode | safe }}
diff --git a/bookwyrm/templates/settings/imports/imports.html b/bookwyrm/templates/settings/imports/imports.html index c6e02f0e1..135af34ed 100644 --- a/bookwyrm/templates/settings/imports/imports.html +++ b/bookwyrm/templates/settings/imports/imports.html @@ -11,6 +11,54 @@ {% block panel %} +
+ {% if site.imports_enabled %} +
+ + + {% trans "Disable starting new imports" %} + + + +
+
+ {% trans "This is only intended to be used when things have gone very wrong with imports and you need to pause the feature while addressing issues." %} + {% trans "While imports are disabled, users will not be allowed to start new imports, but existing imports will not be effected." %} +
+ {% csrf_token %} +
+ +
+
+
+ {% else %} +
+
+ {% trans "Users are currently unable to start new imports" %} +
+ {% csrf_token %} +
+ +
+
+ {% endif %} +
+
    diff --git a/bookwyrm/templates/snippets/footer.html b/bookwyrm/templates/snippets/footer.html index eacb6d6b7..77be88bfe 100644 --- a/bookwyrm/templates/snippets/footer.html +++ b/bookwyrm/templates/snippets/footer.html @@ -24,7 +24,7 @@

- {% trans "Code of Conduct" %} + {% trans "Code of Conduct" %}

{% trans "Privacy Policy" %} diff --git a/bookwyrm/templates/user/user.html b/bookwyrm/templates/user/user.html index 8e61d525d..d67f42fc3 100755 --- a/bookwyrm/templates/user/user.html +++ b/bookwyrm/templates/user/user.html @@ -64,12 +64,14 @@

{% trans "User Activity" %}

+ {% if user.local %} + {% endif %}
{% for activity in activities %}
diff --git a/bookwyrm/templatetags/interaction.py b/bookwyrm/templatetags/interaction.py index 39bf32b63..9c73aa1af 100644 --- a/bookwyrm/templatetags/interaction.py +++ b/bookwyrm/templatetags/interaction.py @@ -42,7 +42,7 @@ def get_relationship(context, user_object): """caches the relationship between the logged in user and another user""" user = context["request"].user return get_or_set( - f"relationship-{user.id}-{user_object.id}", + f"cached-relationship-{user.id}-{user_object.id}", get_relationship_name, user, user_object, diff --git a/bookwyrm/tests/models/test_user_model.py b/bookwyrm/tests/models/test_user_model.py index 37e2192be..80b7a76d4 100644 --- a/bookwyrm/tests/models/test_user_model.py +++ b/bookwyrm/tests/models/test_user_model.py @@ -1,10 +1,12 @@ """ testing models """ import json from unittest.mock import patch +from django.contrib.auth.models import Group from django.test import TestCase import responses from bookwyrm import models +from bookwyrm.management.commands import initdb from bookwyrm.settings import USE_HTTPS, DOMAIN # pylint: disable=missing-class-docstring @@ -12,6 +14,7 @@ from bookwyrm.settings import USE_HTTPS, DOMAIN class User(TestCase): protocol = "https://" if USE_HTTPS else "http://" + # pylint: disable=invalid-name def setUp(self): with patch("bookwyrm.suggested_users.rerank_suggestions_task.delay"), patch( "bookwyrm.activitystreams.populate_stream_task.delay" @@ -25,6 +28,17 @@ class User(TestCase): name="hi", bookwyrm_user=False, ) + self.another_user = models.User.objects.create_user( + f"nutria@{DOMAIN}", + "nutria@nutria.nutria", + "nutriaword", + local=True, + localname="nutria", + name="hi", + bookwyrm_user=False, + ) + initdb.init_groups() + initdb.init_permissions() def test_computed_fields(self): """username instead of id here""" @@ -176,3 +190,41 @@ class User(TestCase): self.assertEqual(activity["type"], "Delete") self.assertEqual(activity["object"], self.user.remote_id) self.assertFalse(self.user.is_active) + + def test_admins_no_admins(self): + """list of admins""" + result = models.User.admins() + self.assertFalse(result.exists()) + + def test_admins_superuser(self): + """list of admins""" + self.user.is_superuser = True + self.user.save(broadcast=False, update_fields=["is_superuser"]) + result = models.User.admins() + self.assertEqual(result.count(), 1) + self.assertEqual(result.first(), self.user) + + def test_admins_superuser_and_mod(self): + """list of admins""" + self.user.is_superuser = True + self.user.save(broadcast=False, update_fields=["is_superuser"]) + group = Group.objects.get(name="moderator") + self.another_user.groups.set([group]) + + results = models.User.admins() + self.assertEqual(results.count(), 2) + self.assertTrue(results.filter(id=self.user.id).exists()) + self.assertTrue(results.filter(id=self.another_user.id).exists()) + + def test_admins_deleted_mod(self): + """list of admins""" + self.user.is_superuser = True + self.user.save(broadcast=False, update_fields=["is_superuser"]) + group = Group.objects.get(name="moderator") + self.another_user.groups.set([group]) + self.another_user.is_active = False + self.another_user.save(broadcast=False, update_fields=None) + + results = models.User.admins() + self.assertEqual(results.count(), 1) + self.assertEqual(results.first(), self.user) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index a1e0ef844..daf05e10e 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -301,6 +301,16 @@ urlpatterns = [ views.ImportList.as_view(), name="settings-imports-complete", ), + re_path( + r"^settings/imports/disable/?$", + views.disable_imports, + name="settings-imports-disable", + ), + re_path( + r"^settings/imports/enable/?$", + views.enable_imports, + name="settings-imports-enable", + ), re_path( r"^settings/celery/?$", views.CeleryStatus.as_view(), name="settings-celery" ), diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index e5b772136..21e33450c 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -10,7 +10,7 @@ from .admin.federation import Federation, FederatedServer from .admin.federation import AddFederatedServer, ImportServerBlocklist from .admin.federation import block_server, unblock_server, refresh_server from .admin.email_blocklist import EmailBlocklist -from .admin.imports import ImportList +from .admin.imports import ImportList, disable_imports, enable_imports from .admin.ip_blocklist import IPBlocklist from .admin.invite import ManageInvites, Invite, InviteRequest from .admin.invite import ManageInviteRequests, ignore_invite_request diff --git a/bookwyrm/views/admin/imports.py b/bookwyrm/views/admin/imports.py index cdb8af751..fe04a0f2b 100644 --- a/bookwyrm/views/admin/imports.py +++ b/bookwyrm/views/admin/imports.py @@ -5,6 +5,7 @@ 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.settings import PAGE_LENGTH @@ -53,3 +54,25 @@ class ImportList(View): import_job = get_object_or_404(models.ImportJob, id=import_id) import_job.stop_job() return redirect("settings-imports") + + +@require_POST +@permission_required("bookwyrm.edit_instance_settings", raise_exception=True) +# pylint: disable=unused-argument +def disable_imports(request): + """When you just need people to please stop starting imports""" + site = models.SiteSettings.objects.get() + site.imports_enabled = False + site.save(update_fields=["imports_enabled"]) + return redirect("settings-imports") + + +@require_POST +@permission_required("bookwyrm.edit_instance_settings", raise_exception=True) +# pylint: disable=unused-argument +def enable_imports(request): + """When you just need people to please stop starting imports""" + site = models.SiteSettings.objects.get() + site.imports_enabled = True + site.save(update_fields=["imports_enabled"]) + return redirect("settings-imports") diff --git a/bookwyrm/views/admin/invite.py b/bookwyrm/views/admin/invite.py index 5c9b61fde..9256094aa 100644 --- a/bookwyrm/views/admin/invite.py +++ b/bookwyrm/views/admin/invite.py @@ -85,6 +85,11 @@ class Invite(View): # post handling is in views.register.Register +@method_decorator(login_required, name="dispatch") +@method_decorator( + permission_required("bookwyrm.create_invites", raise_exception=True), + name="dispatch", +) class ManageInviteRequests(View): """grant invites like the benevolent lord you are""" @@ -177,6 +182,7 @@ class InviteRequest(View): @require_POST +@permission_required("bookwyrm.create_invites", raise_exception=True) def ignore_invite_request(request): """hide an invite request""" invite_request = get_object_or_404( diff --git a/bookwyrm/views/imports/import_data.py b/bookwyrm/views/imports/import_data.py index f9c62a15d..4956bfb7d 100644 --- a/bookwyrm/views/imports/import_data.py +++ b/bookwyrm/views/imports/import_data.py @@ -4,6 +4,7 @@ import datetime from django.contrib.auth.decorators import login_required from django.db.models import Avg, ExpressionWrapper, F, fields +from django.core.exceptions import PermissionDenied from django.core.paginator import Paginator from django.http import HttpResponseBadRequest from django.shortcuts import redirect @@ -54,6 +55,10 @@ class Import(View): def post(self, request): """ingest a goodreads csv""" + site = models.SiteSettings.objects.get() + if not site.imports_enabled: + raise PermissionDenied() + form = forms.ImportForm(request.POST, request.FILES) if not form.is_valid(): return HttpResponseBadRequest()