Repository: sissbruecker/linkding
Pull request #735 (2024-05-14 08:55:57)

Add option for disabling tag grouping


Patch:
diff --git a/bookmarks/migrations/0035_userprofile_tag_grouping.py b/bookmarks/migrations/0035_userprofile_tag_grouping.py
new file mode 100644
index 0000000..dda5bc3
--- /dev/null
+++ b/bookmarks/migrations/0035_userprofile_tag_grouping.py
@@ -0,0 +1,22 @@
+# Generated by Django 5.0.3 on 2024-05-14 08:28
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("bookmarks", "0034_bookmark_preview_image_file_and_more"),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name="userprofile",
+            name="tag_grouping",
+            field=models.CharField(
+                choices=[("alphabetical", "Alphabetical"), ("disabled", "Disabled")],
+                default="alphabetical",
+                max_length=12,
+            ),
+        ),
+    ]
diff --git a/bookmarks/models.py b/bookmarks/models.py
index 199142e..7b0a75c 100644
--- a/bookmarks/models.py
+++ b/bookmarks/models.py
@@ -352,6 +352,12 @@ class UserProfile(models.Model):
         (TAG_SEARCH_STRICT, "Strict"),
         (TAG_SEARCH_LAX, "Lax"),
     ]
+    TAG_GROUPING_ALPHABETICAL = "alphabetical"
+    TAG_GROUPING_DISABLED = "disabled"
+    TAG_GROUPING_CHOICES = [
+        (TAG_GROUPING_ALPHABETICAL, "Alphabetical"),
+        (TAG_GROUPING_DISABLED, "Disabled"),
+    ]
     user = models.OneToOneField(
         get_user_model(), related_name="profile", on_delete=models.CASCADE
     )
@@ -392,6 +398,12 @@ class UserProfile(models.Model):
         blank=False,
         default=TAG_SEARCH_STRICT,
     )
+    tag_grouping = models.CharField(
+        max_length=12,
+        choices=TAG_GROUPING_CHOICES,
+        blank=False,
+        default=TAG_GROUPING_ALPHABETICAL,
+    )
     enable_sharing = models.BooleanField(default=False, null=False)
     enable_public_sharing = models.BooleanField(default=False, null=False)
     enable_favicons = models.BooleanField(default=False, null=False)
@@ -419,6 +431,7 @@ class UserProfileForm(forms.ModelForm):
             "bookmark_link_target",
             "web_archive_integration",
             "tag_search",
+            "tag_grouping",
             "enable_sharing",
             "enable_public_sharing",
             "enable_favicons",
diff --git a/bookmarks/templates/settings/general.html b/bookmarks/templates/settings/general.html
index acafeb0..f04edab 100644
--- a/bookmarks/templates/settings/general.html
+++ b/bookmarks/templates/settings/general.html
@@ -110,6 +110,14 @@
             result will also include bookmarks where a search term matches otherwise.
           </div>
         </div>
+        <div class="form-group">
+          <label for="{{ form.tag_grouping.id_for_label }}" class="form-label">Tag grouping</label>
+          {{ form.tag_grouping|add_class:"form-select width-25 width-sm-100" }}
+          <div class="form-input-hint">
+            In alphabetical mode, tags will be grouped by the first letter.
+            If disabled, tags will not be grouped.
+          </div>
+        </div>
         <div class="form-group">
           <label for="{{ form.enable_favicons.id_for_label }}" class="form-checkbox">
             {{ form.enable_favicons }}
diff --git a/bookmarks/views/partials/contexts.py b/bookmarks/views/partials/contexts.py
index 722f343..f1412b9 100644
--- a/bookmarks/views/partials/contexts.py
+++ b/bookmarks/views/partials/contexts.py
@@ -264,7 +264,16 @@ class TagGroup:
         return f"<{self.char} TagGroup>"
 
     @staticmethod
-    def create_tag_groups(tags: Set[Tag]):
+    def create_tag_groups(mode: str, tags: Set[Tag]):
+        if mode == UserProfile.TAG_GROUPING_ALPHABETICAL:
+            return TagGroup._create_tag_groups_alphabetical(tags)
+        elif mode == UserProfile.TAG_GROUPING_DISABLED:
+            return TagGroup._create_tag_groups_disabled(tags)
+        else:
+            raise ValueError(f"{mode} is not a valid tag grouping mode")
+
+    @staticmethod
+    def _create_tag_groups_alphabetical(tags: Set[Tag]):
         # Ensure groups, as well as tags within groups, are ordered alphabetically
         sorted_tags = sorted(tags, key=lambda x: str.lower(x.name))
         group = None
@@ -289,6 +298,18 @@ class TagGroup:
             groups.append(cjk_group)
         return groups
 
+    @staticmethod
+    def _create_tag_groups_disabled(tags: Set[Tag]):
+        if len(tags) == 0:
+            return []
+
+        sorted_tags = sorted(tags, key=lambda x: str.lower(x.name))
+        group = TagGroup("Ungrouped")
+        for tag in sorted_tags:
+            group.tags.append(tag)
+
+        return [group]
+
 
 class TagCloudContext:
     request_context = RequestContext
@@ -311,7 +332,7 @@ class TagCloudContext:
         )
         has_selected_tags = len(unique_selected_tags) > 0
         unselected_tags = set(unique_tags).symmetric_difference(unique_selected_tags)
-        groups = TagGroup.create_tag_groups(unselected_tags)
+        groups = TagGroup.create_tag_groups(user_profile.tag_grouping, unselected_tags)
 
         self.tags = unique_tags
         self.groups = groups


Test patch:
diff --git a/bookmarks/tests/test_settings_general_view.py b/bookmarks/tests/test_settings_general_view.py
index 5bbba40..575a64b 100644
--- a/bookmarks/tests/test_settings_general_view.py
+++ b/bookmarks/tests/test_settings_general_view.py
@@ -34,6 +34,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "enable_preview_images": False,
             "enable_automatic_html_snapshots": True,
             "tag_search": UserProfile.TAG_SEARCH_STRICT,
+            "tag_grouping": UserProfile.TAG_GROUPING_ALPHABETICAL,
             "display_url": False,
             "display_view_bookmark_action": True,
             "display_edit_bookmark_action": True,
@@ -92,6 +93,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             "enable_preview_images": True,
             "enable_automatic_html_snapshots": False,
             "tag_search": UserProfile.TAG_SEARCH_LAX,
+            "tag_grouping": UserProfile.TAG_GROUPING_DISABLED,
             "display_url": True,
             "display_view_bookmark_action": False,
             "display_edit_bookmark_action": False,
@@ -141,6 +143,7 @@ class SettingsGeneralViewTestCase(TestCase, BookmarkFactoryMixin):
             form_data["enable_automatic_html_snapshots"],
         )
         self.assertEqual(self.user.profile.tag_search, form_data["tag_search"])
+        self.assertEqual(self.user.profile.tag_grouping, form_data["tag_grouping"])
         self.assertEqual(self.user.profile.display_url, form_data["display_url"])
         self.assertEqual(
             self.user.profile.display_view_bookmark_action,
diff --git a/bookmarks/tests/test_tag_cloud_template.py b/bookmarks/tests/test_tag_cloud_template.py
index 2ac61e0..4ac807e 100644
--- a/bookmarks/tests/test_tag_cloud_template.py
+++ b/bookmarks/tests/test_tag_cloud_template.py
@@ -140,6 +140,43 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
             ],
         )
 
+    def test_group_when_grouping_disabled(self):
+        profile = self.get_or_create_test_user().profile
+        profile.tag_grouping = UserProfile.TAG_GROUPING_DISABLED
+        profile.save()
+
+        tags = [
+            self.setup_tag(name="Cockatoo"),
+            self.setup_tag(name="Badger"),
+            self.setup_tag(name="Buffalo"),
+            self.setup_tag(name="Chihuahua"),
+            self.setup_tag(name="Alpaca"),
+            self.setup_tag(name="Coyote"),
+            self.setup_tag(name="Aardvark"),
+            self.setup_tag(name="Bumblebee"),
+            self.setup_tag(name="Armadillo"),
+        ]
+        self.setup_bookmark(tags=tags)
+
+        rendered_template = self.render_template()
+
+        self.assertTagGroups(
+            rendered_template,
+            [
+                [
+                    "Aardvark",
+                    "Alpaca",
+                    "Armadillo",
+                    "Badger",
+                    "Buffalo",
+                    "Bumblebee",
+                    "Chihuahua",
+                    "Cockatoo",
+                    "Coyote",
+                ],
+            ],
+        )
+
     def test_no_duplicate_tag_names(self):
         tags = [
             self.setup_tag(name="shared", user=self.setup_user(enable_sharing=True)),