Repository: sissbruecker/linkding
Pull request #706 (2024-04-17 20:22:07)

Add option for marking bookmarks as unread by default
Fixes https://github.com/sissbruecker/linkding/issues/669

This feature adds an option to the user profile to set the default state for Mark as unread.

* The migration does not update the current default, therefore there is no migration impact.
* I chose to make the field Mark as unread default, rather than a Mark as read default, this is so that it mirror the existing form. e.g if its checked in the setting it will be checked on the bookmarks form.

This is my first proper contribution to this project, so apologies if I have missed anything. I picked up this feature as it seemed like an easy one to implement.


Patch:
diff --git a/bookmarks/migrations/0033_userprofile_default_mark_unread.py b/bookmarks/migrations/0033_userprofile_default_mark_unread.py
new file mode 100644
index 0000000..e5bcfe5
--- /dev/null
+++ b/bookmarks/migrations/0033_userprofile_default_mark_unread.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.0.3 on 2024-04-17 19:27
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookmarks", "0032_html_snapshots_hint_toast"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="default_mark_unread",
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/bookmarks/models.py b/bookmarks/models.py
index c27ae58..9087829 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -399,6 +399,7 @@ class UserProfile(models.Model):
     custom_css = models.TextField(blank=True, null=False)
     search_preferences = models.JSONField(default=dict, null=False)
     enable_automatic_html_snapshots = models.BooleanField(default=True, null=False)
+    default_mark_unread = models.BooleanField(default=False, null=False)
 
 
 class UserProfileForm(forms.ModelForm):
@@ -422,6 +423,7 @@ class UserProfileForm(forms.ModelForm):
             "display_archive_bookmark_action",
             "display_remove_bookmark_action",
             "permanent_notes",
+            "default_mark_unread",
             "custom_css",
         ]
 
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index acec66d..436f404 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -175,6 +175,17 @@
             <button class="btn mt-2" name="create_missing_html_snapshots">Create missing HTML snapshots</button>
           </div>
         {% endif %}
+        <div class="form-group">
+          <label for="{{ form.default_mark_unread.id_for_label }}" class="form-checkbox">
+            {{ form.default_mark_unread }}
+            <i class="form-icon"></i> Create bookmarks as unread by default
+          </label>
+          <div class="form-input-hint">
+            Sets the default state for the "Mark as unread" option when creating a new bookmark.
+            Setting this option will make all new bookmarks default to unread.
+            This can be overridden when creating each new bookmark.
+          </div>
+        </div>
         <div class="form-group">
           <details {% if form.custom_css.value %}open{% endif %}>
             <summary>Custom CSS</summary>
diff --git a/bookmarks/views/bookmarks.py b/bookmarks/views/bookmarks.py
index 4cc4ee5..513f7bc 100644
--- a/bookmarks/views/bookmarks.py
+++ b/bookmarks/views/bookmarks.py
@@ -188,6 +188,7 @@ def new(request):
     initial_title = request.GET.get("title")
     initial_description = request.GET.get("description")
     initial_auto_close = "auto_close" in request.GET
+    initial_mark_unread = request.user.profile.default_mark_unread
 
     if request.method == "POST":
         form = BookmarkForm(request.POST)
@@ -210,6 +211,8 @@ def new(request):
             form.initial["description"] = initial_description
         if initial_auto_close:
             form.initial["auto_close"] = "true"
+        if initial_mark_unread:
+            form.initial["unread"] = "true"
 
     context = {
         "form": form,


Test patch:
diff --git a/bookmarks/tests/test_bookmark_new_view.py b/bookmarks/tests/test_bookmark_new_view.py
index a8ebd44..cef1812 100644
--- a/bookmarks/tests/test_bookmark_new_view.py
+++ b/bookmarks/tests/test_bookmark_new_view.py
@@ -210,3 +210,25 @@ class BookmarkNewViewTestCase(TestCase, BookmarkFactoryMixin):
         response = self.client.get(reverse("bookmarks:edit", args=[bookmark.id]))
 
         self.assertContains(response, '<details class="notes">', count=1)
+
+    def test_should_not_check_unread_by_default(self):
+        response = self.client.get(reverse("bookmarks:new"))
+        html = response.content.decode()
+
+        self.assertInHTML(
+            '<input type="checkbox" name="unread" id="id_unread">',
+            html,
+        )
+
+    def test_should_check_unread_when_configured_in_profile(self):
+        self.user.profile.default_mark_unread = True
+        self.user.profile.save()
+
+        response = self.client.get(reverse("bookmarks:new"))
+        html = response.content.decode()
+
+        self.assertInHTML(
+            '<input type="checkbox" name="unread" value="true" '
+            'id="id_unread" checked="">',
+            html,
+        )
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index 3f6c4c7..2598ae7 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -96,6 +96,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "display_archive_bookmark_action": False,
             "display_remove_bookmark_action": False,
             "permanent_notes": True,
+            "default_mark_unread": True,
             "custom_css": "body { background-color: #000; }",
         }
         response = self.client.post(reverse("bookmarks:settings.general"), form_data)
@@ -155,6 +156,9 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
         self.assertEqual(
             self.user.profile.permanent_notes, form_data["permanent_notes"]
         )
+        self.assertEqual(
+            self.user.profile.default_mark_unread, form_data["default_mark_unread"]
+        )
         self.assertEqual(self.user.profile.custom_css, form_data["custom_css"])
         self.assertSuccessMessage(html, "Profile updated")