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 %}
+
+
+
+ {% 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")