Repository: sissbruecker/linkding
Pull request #638 (2024-02-24 18:11:17)

Added a new Linkding client to community section
Hi. I'm the developer of [Linkdy](https://github.com/JGeek00/linkdy), a Linkding client created with Flutter. The application is still under development, and on a very early stage. The objective is to create a mobile and desktop client, with the same features that the web already has. I have added a new entry on the community section.


Patch:
diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml
index 5e7a896..5bddff2 100644
--- a/.github/workflows/main.yaml
+++ b/.github/workflows/main.yaml
@@ -1,6 +1,6 @@
 name: linkding CI
 
-on: [push]
+on: [push, pull_request]
 
 jobs:
   unit_tests:
diff --git a/README.md b/README.md
index cdd520a..ddbc07a 100644
--- a/README.md
+++ b/README.md
@@ -212,10 +212,12 @@ This section lists community projects around using linkding, in alphabetical ord
 - [feed2linkding](https://codeberg.org/strubbl/feed2linkding) A commandline utility to add all web feed item links to linkding via API call. By [Strubbl](https://github.com/Strubbl)
 - [Helm Chart](https://charts.pascaliske.dev/charts/linkding/) Helm Chart for deploying linkding inside a Kubernetes cluster. By [pascaliske](https://github.com/pascaliske)
 - [iOS Shortcut using API and Tagging](https://gist.github.com/andrewdolphin/a7dff49505e588d940bec55132fab8ad) An iOS shortcut using the Linkding API (no extra logins required) that pulls previously used tags and allows tagging at the time of link creation.
+- [k8s + s3](https://github.com/jzck/linkding-k8s-s3) - Setup for hosting stateless linkding on k8s with sqlite replicated to s3. By [jzck](https://github.com/jzck)
 - [Linka!](https://github.com/cmsax/linka) Web app (also a PWA) for quickly searching & opening bookmarks in linkding, support multi keywords, exclude mode and other advance options. By [cmsax](https://github.com/cmsax)
 - [linkding-cli](https://github.com/bachya/linkding-cli) A command-line interface (CLI) to interact with the linkding REST API. Powered by [aiolinkding](https://github.com/bachya/aiolinkding). By [bachya](https://github.com/bachya)
 - [linkding-extension](https://github.com/jeroenpardon/linkding-extension) Chromium compatible extension that wraps the linkding bookmarklet. Tested with Chrome, Edge, Brave. By [jeroenpardon](https://github.com/jeroenpardon)
 - [linkding-injector](https://github.com/Fivefold/linkding-injector) Injects search results from linkding into the sidebar of search pages like google and duckduckgo. Tested with Firefox and Chrome. By [Fivefold](https://github.com/Fivefold)
+- [Linkdy](https://github.com/JGeek00/linkdy): An open source mobile and desktop (not yet) client created with Flutter. Available at the [Google Play Store](https://play.google.com/store/apps/details?id=com.jgeek00.linkdy). By [JGeek00](https://github.com/JGeek00).
 - [LinkThing](https://apps.apple.com/us/app/linkthing/id1666031776) An iOS client for linkding. By [amoscardino](https://github.com/amoscardino)
 - [Open all links bookmarklet](https://gist.github.com/ukcuddlyguy/336dd7339e6d35fc64a75ccfc9323c66) A browser bookmarklet to open all links on the current Linkding page in new tabs. By [ukcuddlyguy](https://github.com/ukcuddlyguy)
 - [Postman collection](https://gist.github.com/gingerbeardman/f0b42502f3bc9344e92ce63afd4360d3) a group of saved request templates for API testing. By [gingerbeardman](https://github.com/gingerbeardman)
diff --git a/bookmarks/views/partials/contexts.py b/bookmarks/views/partials/contexts.py
index 998f038..700a74d 100644
--- a/bookmarks/views/partials/contexts.py
+++ b/bookmarks/views/partials/contexts.py
@@ -1,5 +1,6 @@
 import urllib.parse
 from typing import Set, List
+import re
 
 from django.core.handlers.wsgi import WSGIRequest
 from django.core.paginator import Paginator
@@ -11,13 +12,13 @@ from bookmarks import utils
 from bookmarks.models import (
     Bookmark,
     BookmarkSearch,
-    BookmarkSearchForm,
     User,
     UserProfile,
     Tag,
 )
 
 DEFAULT_PAGE_SIZE = 30
+CJK_RE = re.compile(r"[\u4e00-\u9fff]+")
 
 
 class BookmarkItem:
@@ -123,13 +124,13 @@ class BookmarkListContext:
         )
 
     def get_base_url(self):
-        raise Exception(f"Must be implemented by subclass")
+        raise Exception("Must be implemented by subclass")
 
     def get_base_action_url(self):
-        raise Exception(f"Must be implemented by subclass")
+        raise Exception("Must be implemented by subclass")
 
     def get_bookmark_query_set(self):
-        raise Exception(f"Must be implemented by subclass")
+        raise Exception("Must be implemented by subclass")
 
 
 class ActiveBookmarkListContext(BookmarkListContext):
@@ -178,23 +179,33 @@ class TagGroup:
         self.tags = []
         self.char = char
 
+    def __repr__(self):
+        return f"<{self.char} TagGroup>"
+
     @staticmethod
     def create_tag_groups(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
         groups = []
+        cjk_used = False
+        cjk_group = TagGroup("Ideographic")
 
         # Group tags that start with a different character than the previous one
         for tag in sorted_tags:
             tag_char = tag.name[0].lower()
-
-            if not group or group.char != tag_char:
+            if CJK_RE.match(tag_char):
+                cjk_used = True
+                cjk_group.tags.append(tag)
+            elif not group or group.char != tag_char:
                 group = TagGroup(tag_char)
                 groups.append(group)
+                group.tags.append(tag)
+            else:
+                group.tags.append(tag)
 
-            group.tags.append(tag)
-
+        if cjk_used:
+            groups.append(cjk_group)
         return groups
 
 
@@ -224,7 +235,7 @@ class TagCloudContext:
         self.has_selected_tags = has_selected_tags
 
     def get_tag_query_set(self):
-        raise Exception(f"Must be implemented by subclass")
+        raise Exception("Must be implemented by subclass")
 
     def get_selected_tags(self, tags: List[Tag]):
         parsed_query = queries.parse_query_string(self.search.q)
diff --git a/requirements.dev.txt b/requirements.dev.txt
index c7bdcc8..7d4b749 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -12,7 +12,7 @@ click==8.1.7
     # via black
 coverage==7.4.1
     # via -r requirements.dev.in
-django==5.0.1
+django==5.0.2
     # via
     #   django-appconf
     #   django-debug-toolbar
diff --git a/requirements.txt b/requirements.txt
index 4e8081d..bea3c7e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -20,7 +20,7 @@ click==8.1.7
     # via waybackpy
 confusable-homoglyphs==3.2.0
     # via django-registration
-django==5.0.1
+django==5.0.2
     # via
     #   -r requirements.in
     #   django-registration


Test patch:
diff --git a/bookmarks/tests/test_tag_cloud_template.py b/bookmarks/tests/test_tag_cloud_template.py
index ca257a3..2ac61e0 100644
--- a/bookmarks/tests/test_tag_cloud_template.py
+++ b/bookmarks/tests/test_tag_cloud_template.py
@@ -39,7 +39,7 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
             group_element = group_elements[group_index]
             link_elements = group_element.select("a")
 
-            self.assertEqual(len(link_elements), len(tags))
+            self.assertEqual(len(link_elements), len(tags), tags)
 
             for tag_index, tag in enumerate(tags, start=0):
                 link_element = link_elements[tag_index]
@@ -50,6 +50,59 @@ class TagCloudTemplateTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
         link_elements = soup.select("p.selected-tags a")
         self.assertEqual(len(link_elements), count)
 
+    def test_cjk_using_single_group(self):
+        """
+        Ideographic characters will be using the same group
+        While other japanese and korean characters will have separate groups.
+        """
+        tags = [
+            self.setup_tag(name="Aardvark"),
+            self.setup_tag(name="Armadillo"),
+            self.setup_tag(name="あひる"),
+            self.setup_tag(name="あきらか"),
+            self.setup_tag(name="アヒル"),
+            self.setup_tag(name="アキラカ"),
+            self.setup_tag(name="ひる"),
+            self.setup_tag(name="アヒル"),
+            self.setup_tag(name="오리"),
+            self.setup_tag(name="물"),
+            self.setup_tag(name="家鴨"),
+            self.setup_tag(name="感じ"),
+        ]
+        self.setup_bookmark(tags=tags)
+        rendered_template = self.render_template()
+
+        self.assertTagGroups(
+            rendered_template,
+            [
+                [
+                    "Aardvark",
+                    "Armadillo",
+                ],
+                [
+                    "あきらか",
+                    "あひる",
+                ],
+                [
+                    "ひる",
+                ],
+                [
+                    "アキラカ",
+                    "アヒル",
+                ],
+                [
+                    "물",
+                ],
+                [
+                    "오리",
+                ],
+                [
+                    "家鴨",
+                    "感じ",
+                ],
+            ],
+        )
+
     def test_group_alphabetically(self):
         tags = [
             self.setup_tag(name="Cockatoo"),