Repository: sissbruecker/linkding
Pull request #652 (2024-03-16 23:56:41)

Add option for custom CSS
Fixes https://github.com/sissbruecker/linkding/issues/621


Patch:
diff --git a/bookmarks/migrations/0026_userprofile_custom_css.py b/bookmarks/migrations/0026_userprofile_custom_css.py
new file mode 100644
index 0000000..f123246
--- /dev/null
+++ b/bookmarks/migrations/0026_userprofile_custom_css.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.2 on 2024-03-16 23:05
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookmarks", "0025_userprofile_search_preferences"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="custom_css",
+            field=models.TextField(blank=True),
+        ),
+    ]
diff --git a/bookmarks/models.py b/bookmarks/models.py
index 87711ce..8714283 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -331,6 +331,7 @@ class UserProfile(models.Model):
     enable_favicons = models.BooleanField(default=False, null=False)
     display_url = models.BooleanField(default=False, null=False)
     permanent_notes = models.BooleanField(default=False, null=False)
+    custom_css = models.TextField(blank=True, null=False)
     search_preferences = models.JSONField(default=dict, null=False)
 
 
@@ -348,6 +349,7 @@ class UserProfileForm(forms.ModelForm):
             "enable_favicons",
             "display_url",
             "permanent_notes",
+            "custom_css",
         ]
 
 
diff --git a/bookmarks/styles/settings.scss b/bookmarks/styles/settings.scss
index b533abb..b62e76f 100644
--- a/bookmarks/styles/settings.scss
+++ b/bookmarks/styles/settings.scss
@@ -7,6 +7,10 @@
     }
   }
 
+  textarea.custom-css {
+    font-family: monospace;
+  }
+
   .input-group > input[type=submit] {
     height: auto;
   }
diff --git a/bookmarks/templates/bookmarks/layout.html b/bookmarks/templates/bookmarks/layout.html
index ff4f336..fca6a9a 100644
--- a/bookmarks/templates/bookmarks/layout.html
+++ b/bookmarks/templates/bookmarks/layout.html
@@ -34,6 +34,9 @@
     <meta name="theme-color" media="(prefers-color-scheme: dark)" content="#161822">
     <meta name="theme-color" media="(prefers-color-scheme: light)" content="#5856e0">
   {% endif %}
+  {% if request.user_profile.custom_css %}
+    <style>{{ request.user_profile.custom_css }}</style>
+  {% endif %}
 </head>
 <body ld-global-shortcuts>
 
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index fa3f1b9..823f0b1 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -124,6 +124,18 @@
               href="{% url 'bookmarks:shared' %}">shared bookmarks page</a>.
           </div>
         </div>
+      <div class="form-group">
+        <details {% if form.custom_css.value %}open{% endif %}>
+          <summary>Custom CSS</summary>
+          <label for="{{ form.custom_css.id_for_label }}" class="text-assistive">Custom CSS</label>
+          <div class="mt-2">
+            {{ form.custom_css|add_class:"form-input custom-css"|attr:"rows:6" }}
+          </div>
+        </details>
+        <div class="form-input-hint">
+          Allows to add custom CSS to the page.
+        </div>
+      </div>
         <div class="form-group">
           <input type="submit" name="update_profile" value="Save" class="btn btn-primary mt-2">
           {% if update_profile_success_message %}
diff --git a/siteroot/settings/base.py b/siteroot/settings/base.py
index 373cfa0..c072392 100644
--- a/siteroot/settings/base.py
+++ b/siteroot/settings/base.py
@@ -131,8 +131,6 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
 
 # Turn off SASS compilation by default
 SASS_PROCESSOR_ENABLED = False
-# Location where generated CSS files are saved
-SASS_PROCESSOR_ROOT = os.path.join(BASE_DIR, "bookmarks", "static")
 
 # Add SASS preprocessor finder to resolve generated CSS
 STATICFILES_FINDERS = [


Test patch:
diff --git a/bookmarks/tests/test_custom_css.py b/bookmarks/tests/test_custom_css.py
new file mode 100644
index 0000000..ce0e91e
--- /dev/null
+++ b/bookmarks/tests/test_custom_css.py
@@ -0,0 +1,21 @@
+from django.test import TestCase
+from django.urls import reverse
+
+from bookmarks.tests.helpers import BookmarkFactoryMixin
+
+
+class CustomCssTestCase(TestCase, BookmarkFactoryMixin):
+    def setUp(self):
+        self.client.force_login(self.get_or_create_test_user())
+
+    def test_does_not_render_custom_style_tag_by_default(self):
+        response = self.client.get(reverse("bookmarks:index"))
+        self.assertNotContains(response, "<style>")
+
+    def test_renders_custom_style_tag_if_user_has_custom_css(self):
+        profile = self.get_or_create_test_user().profile
+        profile.custom_css = "body { background-color: red; }"
+        profile.save()
+
+        response = self.client.get(reverse("bookmarks:index"))
+        self.assertContains(response, "<style>body { background-color: red; }</style>")
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index 6186830..f9ed787 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -32,6 +32,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "tag_search": UserProfile.TAG_SEARCH_STRICT,
             "display_url": False,
             "permanent_notes": False,
+            "custom_css": "",
         }
 
         return {**form_data, **overrides}
@@ -63,6 +64,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "tag_search": UserProfile.TAG_SEARCH_LAX,
             "display_url": True,
             "permanent_notes": True,
+            "custom_css": "body { background-color: #000; }",
         }
         response = self.client.post(reverse("bookmarks:settings.general"), form_data)
         html = response.content.decode()
@@ -93,6 +95,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
         self.assertEqual(
             self.user.profile.permanent_notes, form_data["permanent_notes"]
         )
+        self.assertEqual(self.user.profile.custom_css, form_data["custom_css"])
         self.assertInHTML(
             """
                 <p class="form-input-hint">Profile updated</p>