Repository: sissbruecker/linkding
Pull request #928 (2024-12-16 15:57:46)

Bump nanoid from 3.3.7 to 3.3.8
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.7 to 3.3.8.
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/ai/nanoid/blob/main/CHANGELOG.md">nanoid's changelog</a>.</em></p>
<blockquote>
<h2>3.3.8</h2>
<ul>
<li>Fixed a way to break Nano ID by passing non-integer size (by <a href="https://github.com/myndzi"><code>@​myndzi</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/ai/nanoid/commit/3044cd5e73f4cf31795f61f6e6b961c8c0a5c744"><code>3044cd5</code></a> Release 3.3.8 version</li>
<li><a href="https://github.com/ai/nanoid/commit/4fe34959c34e5b3573889ed4f24fe91d1d3e7231"><code>4fe3495</code></a> Update size limit</li>
<li><a href="https://github.com/ai/nanoid/commit/d643045f40d6dc8afa000a644d857da1436ed08c"><code>d643045</code></a> Fix pool pollution, infinite loop (<a href="https://redirect.github.com/ai/nanoid/issues/510">#510</a>)</li>
<li>See full diff in <a href="https://github.com/ai/nanoid/compare/3.3.7...3.3.8">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=nanoid&package-manager=npm_and_yarn&previous-version=3.3.7&new-version=3.3.8)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

You can trigger a rebase of this PR by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/sissbruecker/linkding/network/alerts).

</details>

> **Note**
> Automatic rebases have been disabled on this pull request as it has been open for over 30 days.



Patch:
diff --git a/bookmarks/frontend/behaviors/dropdown.js b/bookmarks/frontend/behaviors/dropdown.js
index 954ced8..bc4c580 100644
--- a/bookmarks/frontend/behaviors/dropdown.js
+++ b/bookmarks/frontend/behaviors/dropdown.js
@@ -6,23 +6,38 @@ class DropdownBehavior extends Behavior {
     this.opened = false;
     this.onClick = this.onClick.bind(this);
     this.onOutsideClick = this.onOutsideClick.bind(this);
+    this.onEscape = this.onEscape.bind(this);
+    this.onFocusOut = this.onFocusOut.bind(this);
+
+    // Prevent opening the dropdown automatically on focus, so that it only
+    // opens on click then JS is enabled
+    this.element.style.setProperty("--dropdown-focus-display", "none");
+    this.element.addEventListener("keydown", this.onEscape);
+    this.element.addEventListener("focusout", this.onFocusOut);
 
     this.toggle = element.querySelector(".dropdown-toggle");
+    this.toggle.setAttribute("aria-expanded", "false");
     this.toggle.addEventListener("click", this.onClick);
   }
 
   destroy() {
     this.close();
     this.toggle.removeEventListener("click", this.onClick);
+    this.element.removeEventListener("keydown", this.onEscape);
+    this.element.removeEventListener("focusout", this.onFocusOut);
   }
 
   open() {
+    this.opened = true;
     this.element.classList.add("active");
+    this.toggle.setAttribute("aria-expanded", "true");
     document.addEventListener("click", this.onOutsideClick);
   }
 
   close() {
+    this.opened = false;
     this.element.classList.remove("active");
+    this.toggle.setAttribute("aria-expanded", "false");
     document.removeEventListener("click", this.onOutsideClick);
   }
 
@@ -39,6 +54,20 @@ class DropdownBehavior extends Behavior {
       this.close();
     }
   }
+
+  onEscape(event) {
+    if (event.key === "Escape" && this.opened) {
+      event.preventDefault();
+      this.close();
+      this.toggle.focus();
+    }
+  }
+
+  onFocusOut(event) {
+    if (!this.element.contains(event.relatedTarget)) {
+      this.close();
+    }
+  }
 }
 
 registerBehavior("ld-dropdown", DropdownBehavior);
diff --git a/bookmarks/static/robots.txt b/bookmarks/static/robots.txt
new file mode 100644
index 0000000..1f53798
--- /dev/null
+++ b/bookmarks/static/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow: /
diff --git a/bookmarks/styles/theme/dropdowns.css b/bookmarks/styles/theme/dropdowns.css
index 07a1715..1d58ac9 100644
--- a/bookmarks/styles/theme/dropdowns.css
+++ b/bookmarks/styles/theme/dropdowns.css
@@ -1,5 +1,7 @@
 /* Dropdown */
 .dropdown {
+  --dropdown-focus-display: block;
+
   display: inline-block;
   position: relative;
 
@@ -20,9 +22,13 @@
     }
   }
 
-  &.active .menu,
-  .dropdown-toggle:focus + .menu,
-  .menu:hover {
+  &:focus-within .menu {
+    /* Use custom CSS property to allow disabling opening on focus when using JS */
+    display: var(--dropdown-focus-display);
+  }
+
+  &.active .menu {
+    /* Always show menu when class is added through JS */
     display: block;
   }
 
diff --git a/bookmarks/templates/bookmarks/nav_menu.html b/bookmarks/templates/bookmarks/nav_menu.html
index e817361..0667fbc 100644
--- a/bookmarks/templates/bookmarks/nav_menu.html
+++ b/bookmarks/templates/bookmarks/nav_menu.html
@@ -3,11 +3,11 @@
   {# Basic menu list #}
   <div class="hide-md">
     <a href="{% url 'bookmarks:new' %}" class="btn btn-primary mr-2">Add bookmark</a>
-    <div class="dropdown">
+    <div ld-dropdown class="dropdown">
       <button class="btn btn-link dropdown-toggle" tabindex="0">
         Bookmarks
       </button>
-      <ul class="menu">
+      <ul class="menu" role="list">
         <li class="menu-item">
           <a href="{% url 'bookmarks:index' %}" class="menu-link">Active</a>
         </li>
@@ -49,7 +49,7 @@
         </svg>
       </button>
       <!-- menu component -->
-      <ul class="menu">
+      <ul class="menu" role="list">
         <li class="menu-item">
           <a href="{% url 'bookmarks:index' %}" class="menu-link">Bookmarks</a>
         </li>
diff --git a/bookmarks/templates/bookmarks/search.html b/bookmarks/templates/bookmarks/search.html
index ecc533e..009bdc6 100644
--- a/bookmarks/templates/bookmarks/search.html
+++ b/bookmarks/templates/bookmarks/search.html
@@ -15,7 +15,8 @@
     {% endfor %}
   </form>
   <div ld-dropdown class="search-options dropdown dropdown-right">
-    <button type="button" class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}">
+    <button type="button" aria-label="Search preferences"
+            class="btn dropdown-toggle{% if search.has_modified_preferences %} badge{% endif %}">
       <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" stroke-width="2"
            stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round">
         <path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
@@ -41,8 +42,11 @@
           </div>
         {% endif %}
         {% if 'shared' in preferences_form.editable_fields %}
-          <div class="form-group radio-group">
-            <div class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}">Shared filter</div>
+          <div class="form-group radio-group" role="radiogroup" aria-labelledby="search-shared-label">
+            <label id="search-shared-label"
+                   class="form-label{% if 'shared' in search.modified_params %} text-bold{% endif %}">
+              Shared filter
+            </label>
             {% for radio in preferences_form.shared %}
               <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
                 {{ radio.tag }}
@@ -53,8 +57,11 @@
           </div>
         {% endif %}
         {% if 'unread' in preferences_form.editable_fields %}
-          <div class="form-group radio-group">
-            <div class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}">Unread filter</div>
+          <div class="form-group radio-group" role="radiogroup" aria-labelledby="search-unread-label">
+            <label id="search-unread-label"
+                   class="form-label{% if 'unread' in search.modified_params %} text-bold{% endif %}">
+              Unread filter
+            </label>
             {% for radio in preferences_form.unread %}
               <label for="{{ radio.id_for_label }}" class="form-radio form-inline">
                 {{ radio.tag }}
diff --git a/docs/package-lock.json b/docs/package-lock.json
index 28cc8fc..b60e6fa 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -7001,9 +7001,10 @@
       }
     },
     "node_modules/vite": {
-      "version": "5.4.9",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz",
-      "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==",
+      "version": "5.4.14",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
+      "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
+      "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
         "postcss": "^8.4.43",
diff --git a/docs/src/content/docs/community.md b/docs/src/content/docs/community.md
index 60d394d..e40647b 100644
--- a/docs/src/content/docs/community.md
+++ b/docs/src/content/docs/community.md
@@ -13,10 +13,14 @@ This section lists community projects around using linkding, in alphabetical ord
 - [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-archiver](https://github.com/sebw/linkding-archiver) A Python application that integrates with SingleFile and Tube Archivist to archive your links and videos. By [sebw](https://github.com/sebw)  
 - [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-healthcheck](https://github.com/sebw/linkding-healthcheck) A Go application that checks the health of your bookmarks and add a tag on dead and problematic URLs. By [sebw](https://github.com/sebw) 
 - [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).
+- [linkding-reminder](https://github.com/sebw/linkding-reminder) A Python application that will send an email reminder for links with a specific tag. By [sebw](https://github.com/sebw) 
+- [linkding-rs](https://github.com/zbrox/linkding-rs) A Rust client library to interact with the linkding REST API with cross platform support to be easily used in Android or iOS apps. By [zbrox](https://github.com/zbrox)
+- [Linkdy](https://github.com/JGeek00/linkdy): An open-source Android and iOS client created with Flutter. Available on the [Google Play Store](https://play.google.com/store/apps/details?id=com.jgeek00.linkdy) and [App Store](https://apps.apple.com/us/app/linkdy/id6479930976). 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)
 - [Pinkt](https://github.com/fibelatti/pinboard-kotlin) An Android client for linkding. By [fibelatti](https://github.com/fibelatti)
diff --git a/package-lock.json b/package-lock.json
index 72a0524..ea5f9ee 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "linkding",
-  "version": "1.35.0",
+  "version": "1.36.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "linkding",
-      "version": "1.35.0",
+      "version": "1.36.0",
       "license": "MIT",
       "dependencies": {
         "@hotwired/turbo": "^8.0.6",
@@ -1310,9 +1310,9 @@
       }
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "funding": [
         {
           "type": "github",
diff --git a/requirements.dev.txt b/requirements.dev.txt
index 2e61706..5831847 100644
--- a/requirements.dev.txt
+++ b/requirements.dev.txt
@@ -12,7 +12,7 @@ click==8.1.7
     # via black
 coverage==7.6.1
     # via -r requirements.dev.in
-django==5.1.1
+django==5.1.5
     # via django-debug-toolbar
 django-debug-toolbar==4.4.6
     # via -r requirements.dev.in
diff --git a/requirements.txt b/requirements.txt
index 5bd253d..2d5958a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -27,7 +27,7 @@ cryptography==43.0.1
     #   josepy
     #   mozilla-django-oidc
     #   pyopenssl
-django==5.1.1
+django==5.1.5
     # via
     #   -r requirements.in
     #   django-registration
diff --git a/uwsgi.ini b/uwsgi.ini
index e9f7b9b..6e2d752 100644
--- a/uwsgi.ini
+++ b/uwsgi.ini
@@ -4,6 +4,7 @@ env = DJANGO_SETTINGS_MODULE=siteroot.settings.prod
 static-map = /static=static
 static-map = /static=data/favicons
 static-map = /static=data/previews
+static-map = /robots.txt=static/robots.txt
 processes = 2
 threads = 2
 pidfile = /tmp/linkding.pid
@@ -18,6 +19,7 @@ if-env = LD_CONTEXT_PATH
 static-map = /%(_)static=static
 static-map = /%(_)static=data/favicons
 static-map = /%(_)static=data/previews
+static-map = /%(_)robots.txt=static/robots.txt
 endif =
 
 if-env = LD_REQUEST_TIMEOUT


Test patch:
diff --git a/bookmarks/tests/test_bookmark_search_tag.py b/bookmarks/tests/test_bookmark_search_tag.py
index e8c016f..eb77675 100644
--- a/bookmarks/tests/test_bookmark_search_tag.py
+++ b/bookmarks/tests/test_bookmark_search_tag.py
@@ -71,19 +71,15 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
         radios = form.select(f'input[name="{name}"][type="radio"]')
         self.assertTrue(len(radios) == 0)
 
-    def assertUnmodifiedLabel(self, html: str, text: str, id: str = ""):
-        id_attr = f'for="{id}"' if id else ""
-        tag = "label" if id else "div"
-        needle = f'<{tag} class="form-label" {id_attr}>{text}</{tag}>'
+    def assertUnmodifiedLabel(self, html: str, text: str):
+        soup = self.make_soup(html)
+        label = soup.find("label", string=lambda s: s and s.strip() == text)
+        self.assertEqual(label["class"], ["form-label"])
 
-        self.assertInHTML(needle, html)
-
-    def assertModifiedLabel(self, html: str, text: str, id: str = ""):
-        id_attr = f'for="{id}"' if id else ""
-        tag = "label" if id else "div"
-        needle = f'<{tag} class="form-label text-bold" {id_attr}>{text}</{tag}>'
-
-        self.assertInHTML(needle, html)
+    def assertModifiedLabel(self, html: str, text: str):
+        soup = self.make_soup(html)
+        label = soup.find("label", string=lambda s: s and s.strip() == text)
+        self.assertEqual(label["class"], ["form-label", "text-bold"])
 
     def test_search_form_inputs(self):
         # Without params
@@ -190,54 +186,53 @@ class BookmarkSearchTagTest(TestCase, BookmarkFactoryMixin, HtmlTestMixin):
         # Without modifications
         url = "/test"
         rendered_template = self.render_template(url)
+        soup = self.make_soup(rendered_template)
+        button = soup.select_one("button[aria-label='Search preferences']")
 
-        self.assertIn(
-            '<button type="button" class="btn dropdown-toggle">', rendered_template
-        )
+        self.assertNotIn("badge", button["class"])
 
         # With modifications
         url = "/test?sort=title_asc"
         rendered_template = self.render_template(url)
+        soup = self.make_soup(rendered_template)
+        button = soup.select_one("button[aria-label='Search preferences']")
 
-        self.assertIn(
-            '<button type="button" class="btn dropdown-toggle badge">',
-            rendered_template,
-        )
+        self.assertIn("badge", button["class"])
 
         # Ignores non-preferences modifications
         url = "/test?q=foo&user=john"
         rendered_template = self.render_template(url)
+        soup = self.make_soup(rendered_template)
+        button = soup.select_one("button[aria-label='Search preferences']")
 
-        self.assertIn(
-            '<button type="button" class="btn dropdown-toggle">', rendered_template
-        )
+        self.assertNotIn("badge", button["class"])
 
     def test_modified_labels(self):
         # Without modifications
         url = "/test"
         rendered_template = self.render_template(url)
 
-        self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
+        self.assertUnmodifiedLabel(rendered_template, "Sort by")
         self.assertUnmodifiedLabel(rendered_template, "Shared filter")
         self.assertUnmodifiedLabel(rendered_template, "Unread filter")
 
         # Modified sort
         url = "/test?sort=title_asc"
         rendered_template = self.render_template(url)
-        self.assertModifiedLabel(rendered_template, "Sort by", "id_sort")
+        self.assertModifiedLabel(rendered_template, "Sort by")
         self.assertUnmodifiedLabel(rendered_template, "Shared filter")
         self.assertUnmodifiedLabel(rendered_template, "Unread filter")
 
         # Modified shared
         url = "/test?shared=yes"
         rendered_template = self.render_template(url)
-        self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
+        self.assertUnmodifiedLabel(rendered_template, "Sort by")
         self.assertModifiedLabel(rendered_template, "Shared filter")
         self.assertUnmodifiedLabel(rendered_template, "Unread filter")
 
         # Modified unread
         url = "/test?unread=yes"
         rendered_template = self.render_template(url)
-        self.assertUnmodifiedLabel(rendered_template, "Sort by", "id_sort")
+        self.assertUnmodifiedLabel(rendered_template, "Sort by")
         self.assertUnmodifiedLabel(rendered_template, "Shared filter")
         self.assertModifiedLabel(rendered_template, "Unread filter")