diff --git a/bookwyrm/migrations/0188_theme_loads.py b/bookwyrm/migrations/0188_theme_loads.py new file mode 100644 index 000000000..846aaf549 --- /dev/null +++ b/bookwyrm/migrations/0188_theme_loads.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.23 on 2023-11-20 18:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bookwyrm", "0187_partial_publication_dates"), + ] + + operations = [ + migrations.AddField( + model_name="theme", + name="loads", + field=models.BooleanField(blank=True, null=True), + ), + ] diff --git a/bookwyrm/models/site.py b/bookwyrm/models/site.py index cce055999..bd53f1f07 100644 --- a/bookwyrm/models/site.py +++ b/bookwyrm/models/site.py @@ -150,6 +150,7 @@ class Theme(SiteModel): created_date = models.DateTimeField(auto_now_add=True) name = models.CharField(max_length=50, unique=True) path = models.CharField(max_length=50, unique=True) + loads = models.BooleanField(null=True, blank=True) def __str__(self): # pylint: disable=invalid-str-returned diff --git a/bookwyrm/templates/403.html b/bookwyrm/templates/403.html new file mode 100644 index 000000000..0b78bc6b8 --- /dev/null +++ b/bookwyrm/templates/403.html @@ -0,0 +1,20 @@ +{% extends 'layout.html' %} +{% load i18n %} +{% load utilities %} + +{% block title %}{% trans "Oh no!" %}{% endblock %} + +{% block content %} +
+

{% trans "Permission Denied" %}

+

+ {% blocktrans trimmed with level=request.user|get_user_permission %} + You do not have permission to view this page or perform this action. Your user permission level is {{ level }}. + {% endblocktrans %} +

+

{% trans "If you think you should have access, please speak to your BookWyrm server administrator." %} +

+ +
+{% endblock %} + diff --git a/bookwyrm/templates/settings/themes.html b/bookwyrm/templates/settings/themes.html index d27aeb0ce..c077fa5e3 100644 --- a/bookwyrm/templates/settings/themes.html +++ b/bookwyrm/templates/settings/themes.html @@ -12,6 +12,15 @@ {% endblock %} {% block panel %} +{% if broken_theme %} +
+ + + {% trans "One of your themes appears to be broken. Selecting this theme will make the application unusable." %} + +
+{% endif %} + {% if success %}
@@ -98,6 +107,9 @@ {% trans "Actions" %} + + {% trans "Status" %} + {% for theme in themes %} @@ -112,6 +124,37 @@ + + {% if theme.loads is None %} + +
+ {% csrf_token %} + +
+ + {% elif not theme.loads %} + + + + + {% trans "Broken theme" %} + + + + {% else %} + + + + + {% trans "Loaded successfully" %} + + + + {% endif %} + {% endfor %} diff --git a/bookwyrm/templatetags/utilities.py b/bookwyrm/templatetags/utilities.py index 448a4e9e4..6618b55f6 100644 --- a/bookwyrm/templatetags/utilities.py +++ b/bookwyrm/templatetags/utilities.py @@ -132,6 +132,7 @@ def id_to_username(user_id): def get_file_size(file): """display the size of a file in human readable terms""" + try: raw_size = os.stat(file.path).st_size if raw_size < 1024: @@ -144,7 +145,14 @@ def get_file_size(file): except Exception: # pylint: disable=broad-except return "" - + +@register.filter(name="get_user_permission") +def get_user_permission(user): + """given a user, return their permission level""" + + return user.groups.first() or "User" + + @register.filter(name="is_instance_admin") def is_instance_admin(localname): """Returns a boolean indicating whether the user is the instance admin account""" diff --git a/bookwyrm/tests/views/admin/test_themes.py b/bookwyrm/tests/views/admin/test_themes.py index bc6377681..296cd4d8d 100644 --- a/bookwyrm/tests/views/admin/test_themes.py +++ b/bookwyrm/tests/views/admin/test_themes.py @@ -86,3 +86,25 @@ class AdminThemesViews(TestCase): with self.assertRaises(PermissionDenied): view(request) + + def test_test_theme(self): + """Testing testing testing test""" + theme = models.Theme.objects.first() + self.assertIsNone(theme.loads) + request = self.factory.post("") + request.user = self.local_user + + views.test_theme(request, theme.id) + theme.refresh_from_db() + self.assertTrue(theme.loads) + + def test_test_theme_broken(self): + """Testing test for testing when it's a bad theme""" + theme = models.Theme.objects.create(name="bad theme", path="dsf/sdf/sdf.sdf") + self.assertIsNone(theme.loads) + request = self.factory.post("") + request.user = self.local_user + + views.test_theme(request, theme.id) + theme.refresh_from_db() + self.assertIs(False, theme.loads) diff --git a/bookwyrm/urls.py b/bookwyrm/urls.py index 10539c26a..6e75b6390 100644 --- a/bookwyrm/urls.py +++ b/bookwyrm/urls.py @@ -109,6 +109,11 @@ urlpatterns = [ views.delete_theme, name="settings-themes-delete", ), + re_path( + r"^settings/themes/(?P\d+)/test/?$", + views.test_theme, + name="settings-themes-test", + ), re_path( r"^settings/announcements/?$", views.Announcements.as_view(), @@ -813,3 +818,6 @@ urlpatterns.extend(staticfiles_urlpatterns()) # pylint: disable=invalid-name handler500 = "bookwyrm.views.server_error" + +# pylint: disable=invalid-name +handler403 = "bookwyrm.views.permission_denied" diff --git a/bookwyrm/views/__init__.py b/bookwyrm/views/__init__.py index ad2a4c968..7194ab538 100644 --- a/bookwyrm/views/__init__.py +++ b/bookwyrm/views/__init__.py @@ -32,7 +32,7 @@ from .admin.reports import ( moderator_delete_user, ) from .admin.site import Site, Registration, RegistrationLimited -from .admin.themes import Themes, delete_theme +from .admin.themes import Themes, delete_theme, test_theme from .admin.user_admin import UserAdmin, UserAdminList, ActivateUserAdmin # user preferences @@ -169,3 +169,4 @@ from .annual_summary import ( summary_revoke_key, ) from .server_error import server_error +from .permission_denied import permission_denied diff --git a/bookwyrm/views/admin/themes.py b/bookwyrm/views/admin/themes.py index 5658d243a..284a90833 100644 --- a/bookwyrm/views/admin/themes.py +++ b/bookwyrm/views/admin/themes.py @@ -6,6 +6,8 @@ from django.utils.decorators import method_decorator from django.views import View from django.views.decorators.http import require_POST +from sass_processor.processor import sass_processor + from bookwyrm import forms, models @@ -40,6 +42,7 @@ class Themes(View): def get_view_data(): """data for view""" return { + "broken_theme": models.Theme.objects.filter(loads=False).exists(), "themes": models.Theme.objects.all(), "theme_form": forms.ThemeForm(), } @@ -52,3 +55,20 @@ def delete_theme(request, theme_id): """Remove a theme""" get_object_or_404(models.Theme, id=theme_id).delete() return redirect("settings-themes") + + +@require_POST +@permission_required("bookwyrm.system_administration", raise_exception=True) +# pylint: disable=unused-argument +def test_theme(request, theme_id): + """Remove a theme""" + theme = get_object_or_404(models.Theme, id=theme_id) + + try: + sass_processor(theme.path) + theme.loads = True + except Exception: # pylint: disable=broad-except + theme.loads = False + + theme.save() + return redirect("settings-themes") diff --git a/bookwyrm/views/permission_denied.py b/bookwyrm/views/permission_denied.py new file mode 100644 index 000000000..9e62b0933 --- /dev/null +++ b/bookwyrm/views/permission_denied.py @@ -0,0 +1,15 @@ +"""custom 403 handler to enable context processors""" + +from django.http import HttpResponse +from django.template.response import TemplateResponse + +from .helpers import is_api_request + + +def permission_denied(request, exception): # pylint: disable=unused-argument + """permission denied page""" + + if request.method == "POST" or is_api_request(request): + return HttpResponse(status=403) + + return TemplateResponse(request, "403.html")